Skip to main content

formualizer_eval/builtins/lookup/
dynamic.rs

1//! Dynamic / modern lookup & array helpers: XLOOKUP, FILTER, UNIQUE (initial sprint subset)
2//!
3//! Notes / Simplifications (documented for future refinement):
4//! - XLOOKUP supports: lookup_value, lookup_array, return_array, [if_not_found], [match_mode], [search_mode]
5//!   * match_mode: 0 exact (default), -1 exact-or-next-smaller, 1 exact-or-next-larger, 2 wildcard (basic * ?)
6//!   * search_mode: 1 forward (default), -1 reverse; (2 / -2 binary not yet implemented -> treated as 1 / -1)
7//!   * Wildcard mode (2) currently case-insensitive ASCII only; TODO: full Excel semantics, escape handling (~)
8//! - FILTER supports: array, include, [if_empty]; Shapes must be broadcast-compatible by rows (include is 1-D).
9//!   * include may be vertical column vector OR same sized 2D; we reduce any non-zero truthy cell to include row.
10//!   * if_empty omitted -> #CALC! per Excel when no matches.
11//! - UNIQUE supports: array, [by_col], [exactly_once]
12//!   * by_col TRUE -> operate column-wise returning unique columns (NYI -> returns #N/IMPL! if TRUE)
13//!   * exactly_once TRUE returns only values with count == 1 (supported in row-wise primitive set)
14//! - All functions return Array literal values (spills) – engine handles spill placement later.
15//!
16//! TODO(backlog):
17//! - Binary search for XLOOKUP approximate modes; currently linear scan.
18//! - Better type coercion parity with Excel (booleans/text vs numbers nuances).
19//! - Match unsorted detection for approximate modes (#N/A) and wildcard escaping.
20//! - PERFORMANCE: streaming FILTER without full materialization; UNIQUE using smallvec for tiny sets.
21
22use super::super::utils::collapse_if_scalar;
23use super::lookup_utils::{cmp_for_lookup, equals_maybe_wildcard, value_to_f64_lenient};
24use crate::args::{ArgSchema, CoercionPolicy, ShapeKind};
25use crate::function::Function; // FnCaps imported via macro
26use crate::traits::{ArgumentHandle, FunctionContext};
27use formualizer_common::{ArgKind, ExcelError, ExcelErrorKind, LiteralValue};
28use formualizer_macros::func_caps;
29use std::collections::HashMap;
30
31/* ───────────────────────── helpers ───────────────────────── */
32
33pub fn super_wildcard_match(pattern: &str, text: &str) -> bool {
34    // public for shared lookup utils
35    // Excel-style wildcards with escape (~): * any seq, ? single char, ~ escapes next (*, ?, ~)
36    // Implement non-recursive DP for performance & to support escapes.
37    #[derive(Clone, Copy, Debug)]
38    enum Token<'a> {
39        AnySeq,
40        AnyChar,
41        Lit(&'a str),
42    }
43    let mut tokens: Vec<Token> = Vec::new();
44    let mut i = 0;
45    let bytes = pattern.as_bytes();
46    let mut lit_start = 0;
47    while i < bytes.len() {
48        match bytes[i] {
49            b'~' => {
50                // escape next if present
51                if i + 1 < bytes.len() {
52                    // flush pending literal
53                    if lit_start < i {
54                        tokens.push(Token::Lit(&pattern[lit_start..i]));
55                    }
56                    tokens.push(Token::Lit(&pattern[i + 1..i + 2]));
57                    i += 2;
58                    lit_start = i;
59                } else {
60                    // trailing ~ treated literal
61                    i += 1;
62                }
63            }
64            b'*' => {
65                if lit_start < i {
66                    tokens.push(Token::Lit(&pattern[lit_start..i]));
67                }
68                tokens.push(Token::AnySeq);
69                i += 1;
70                lit_start = i;
71            }
72            b'?' => {
73                if lit_start < i {
74                    tokens.push(Token::Lit(&pattern[lit_start..i]));
75                }
76                tokens.push(Token::AnyChar);
77                i += 1;
78                lit_start = i;
79            }
80            _ => i += 1,
81        }
82    }
83    if lit_start < bytes.len() {
84        tokens.push(Token::Lit(&pattern[lit_start..]));
85    }
86    // Simplify consecutive AnySeq
87    let mut compact: Vec<Token> = Vec::new();
88    for t in tokens {
89        match t {
90            Token::AnySeq => {
91                if !matches!(compact.last(), Some(Token::AnySeq)) {
92                    compact.push(t);
93                }
94            }
95            _ => compact.push(t),
96        }
97    }
98    // Backtracking matcher
99    fn match_tokens<'a>(tokens: &[Token<'a>], text: &str) -> bool {
100        fn eq_icase(a: &str, b: &str) -> bool {
101            a.eq_ignore_ascii_case(b)
102        }
103        // Convert Lit tokens into lowercase for quick compare
104        let mut ti = 0;
105        let tb = tokens;
106        // Use manual stack for backtracking when encountering AnySeq
107        let mut backtrack: Vec<(usize, usize)> = Vec::new(); // (token_index, text_index after consuming 1 more char by *)
108        let text_bytes = text.as_bytes();
109        let mut si = 0; // text index
110        loop {
111            if ti == tb.len() {
112                // tokens consumed
113                if si == text_bytes.len() {
114                    return true;
115                }
116                // Maybe backtrack
117            } else {
118                match tb[ti] {
119                    Token::AnySeq => {
120                        // try to match zero chars first
121                        ti += 1;
122                        backtrack.push((ti - 1, si + 1));
123                        continue;
124                    }
125                    Token::AnyChar => {
126                        if si < text_bytes.len() {
127                            ti += 1;
128                            si += 1;
129                            continue;
130                        }
131                    }
132                    Token::Lit(l) => {
133                        let l_len = l.len();
134                        if si + l_len <= text_bytes.len() && eq_icase(&text[si..si + l_len], l) {
135                            ti += 1;
136                            si += l_len;
137                            continue;
138                        }
139                    }
140                }
141            }
142            // failed match; attempt backtrack
143            if let Some((tok_star, new_si)) = backtrack.pop() {
144                if new_si <= text_bytes.len() {
145                    ti = tok_star + 1;
146                    si = new_si;
147                    continue;
148                } else {
149                    continue;
150                }
151            }
152            return false;
153        }
154    }
155    match_tokens(&compact, text)
156}
157
158/* ───────────────────────── XLOOKUP() ───────────────────────── */
159
160#[derive(Debug)]
161pub struct XLookupFn;
162
163/// Looks up a value in one array and returns the aligned value from another array.
164///
165/// `XLOOKUP` supports exact, approximate, and wildcard matching with forward or reverse search.
166///
167/// # Remarks
168/// - Defaults: `match_mode=0` (exact), `search_mode=1` (first-to-last).
169/// - `if_not_found` is optional; if omitted and no match exists, returns `#N/A`.
170/// - `match_mode`: `0` exact, `-1` exact-or-next-smaller, `1` exact-or-next-larger, `2` wildcard.
171/// - `search_mode`: `1` forward, `-1` reverse. Other modes are accepted with current fallback behavior.
172/// - `lookup_array` must be 1D. Invalid shape returns `#VALUE!`.
173/// - If `return_array` is multi-column or multi-row, the matched row/column is returned as a spill.
174///
175/// # Examples
176/// ```yaml,sandbox
177/// title: "Exact key lookup with fallback"
178/// grid:
179///   A1: "id"
180///   A2: 101
181///   A3: 102
182///   B1: "name"
183///   B2: "Ana"
184///   B3: "Bo"
185/// formula: '=XLOOKUP(102,A2:A3,B2:B3,"Missing")'
186/// expected: "Bo"
187/// ```
188///
189/// ```yaml,sandbox
190/// title: "Return a full row from a matched key"
191/// grid:
192///   A1: 1
193///   A2: 2
194///   B1: "East"
195///   C1: 120
196///   B2: "West"
197///   C2: 140
198/// formula: '=XLOOKUP(2,A1:A2,B1:C2)'
199/// expected: [["West",140]]
200/// ```
201///
202/// ```yaml,docs
203/// related:
204///   - XMATCH
205///   - MATCH
206///   - FILTER
207/// faq:
208///   - q: "How do match_mode and search_mode interact?"
209///     a: "match_mode controls exact/approximate/wildcard behavior, while search_mode controls scan direction; reverse search (-1) returns the last matching position."
210///   - q: "What happens when no match is found?"
211///     a: "If if_not_found is provided, XLOOKUP returns that value; otherwise it returns #N/A."
212///   - q: "Why do I get #VALUE! from XLOOKUP on valid ranges?"
213///     a: "The lookup_array must be one-dimensional (single row or single column); multi-row-and-column lookup ranges return #VALUE!."
214/// ```
215/// [formualizer-docgen:schema:start]
216/// Name: XLOOKUP
217/// Type: XLookupFn
218/// Min args: 3
219/// Max args: variadic
220/// Variadic: true
221/// Signature: XLOOKUP(arg1: any@scalar, arg2: range@range, arg3: range@range, arg4?: any@scalar, arg5?: number@scalar, arg6?...: number@scalar)
222/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg3{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg4{kinds=any,required=false,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg5{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg6{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}
223/// Caps: PURE, LOOKUP
224/// [formualizer-docgen:schema:end]
225impl Function for XLookupFn {
226    func_caps!(PURE, LOOKUP);
227    fn name(&self) -> &'static str {
228        "XLOOKUP"
229    }
230    fn min_args(&self) -> usize {
231        3
232    }
233    fn variadic(&self) -> bool {
234        true
235    }
236    fn arg_schema(&self) -> &'static [ArgSchema] {
237        use once_cell::sync::Lazy;
238        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
239            vec![
240                // lookup_value
241                ArgSchema {
242                    kinds: smallvec::smallvec![ArgKind::Any],
243                    required: true,
244                    by_ref: false,
245                    shape: ShapeKind::Scalar,
246                    coercion: CoercionPolicy::None,
247                    max: None,
248                    repeating: None,
249                    default: None,
250                },
251                // lookup_array (range)
252                ArgSchema {
253                    kinds: smallvec::smallvec![ArgKind::Range],
254                    required: true,
255                    by_ref: true,
256                    shape: ShapeKind::Range,
257                    coercion: CoercionPolicy::None,
258                    max: None,
259                    repeating: None,
260                    default: None,
261                },
262                // return_array (range)
263                ArgSchema {
264                    kinds: smallvec::smallvec![ArgKind::Range],
265                    required: true,
266                    by_ref: true,
267                    shape: ShapeKind::Range,
268                    coercion: CoercionPolicy::None,
269                    max: None,
270                    repeating: None,
271                    default: None,
272                },
273                // if_not_found (any optional)
274                ArgSchema {
275                    kinds: smallvec::smallvec![ArgKind::Any],
276                    required: false,
277                    by_ref: false,
278                    shape: ShapeKind::Scalar,
279                    coercion: CoercionPolicy::None,
280                    max: None,
281                    repeating: None,
282                    default: None,
283                },
284                // match_mode (number) default 0
285                ArgSchema {
286                    kinds: smallvec::smallvec![ArgKind::Number],
287                    required: false,
288                    by_ref: false,
289                    shape: ShapeKind::Scalar,
290                    coercion: CoercionPolicy::NumberLenientText,
291                    max: None,
292                    repeating: None,
293                    default: Some(LiteralValue::Int(0)),
294                },
295                // search_mode (number) default 1
296                ArgSchema {
297                    kinds: smallvec::smallvec![ArgKind::Number],
298                    required: false,
299                    by_ref: false,
300                    shape: ShapeKind::Scalar,
301                    coercion: CoercionPolicy::NumberLenientText,
302                    max: None,
303                    repeating: None,
304                    default: Some(LiteralValue::Int(1)),
305                },
306            ]
307        });
308        &SCHEMA
309    }
310    fn eval<'a, 'b, 'c>(
311        &self,
312        args: &'c [ArgumentHandle<'a, 'b>],
313        _ctx: &dyn FunctionContext<'b>,
314    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
315        if args.len() < 3 {
316            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
317                ExcelError::new(ExcelErrorKind::Value),
318            )));
319        }
320        let lookup_value = args[0].value()?.into_literal();
321        if let LiteralValue::Error(ref e) = lookup_value {
322            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
323                e.clone(),
324            )));
325        }
326        let lookup_view = match args[1].range_view() {
327            Ok(v) => v,
328            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
329        };
330        let ret_view = match args[2].range_view() {
331            Ok(v) => v,
332            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
333        };
334
335        let (lookup_rows, lookup_cols) = lookup_view.dims();
336        let (ret_rows, ret_cols) = ret_view.dims();
337
338        // XLOOKUP requires a 1-D lookup array (single row or single column).
339        // If the lookup range is completely empty (used-region trimmed to 0),
340        // fall back to the return range's used-region length and treat missing lookup
341        // cells as Empty.
342        let vertical = if lookup_cols == 1 {
343            true
344        } else if lookup_rows == 1 {
345            false
346        } else if lookup_rows == 0 && lookup_cols == 0 {
347            if ret_cols == 1 {
348                true
349            } else if ret_rows == 1 {
350                false
351            } else {
352                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
353                    ExcelError::new(ExcelErrorKind::Value),
354                )));
355            }
356        } else {
357            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
358                ExcelError::new(ExcelErrorKind::Value),
359            )));
360        };
361
362        let lookup_len = {
363            let raw = if vertical { lookup_rows } else { lookup_cols };
364            if raw == 0 {
365                if vertical { ret_rows } else { ret_cols }
366            } else {
367                raw
368            }
369        };
370
371        if lookup_len == 0 {
372            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
373                ExcelError::new(ExcelErrorKind::Na),
374            )));
375        }
376
377        let match_mode = if args.len() >= 5 {
378            match args[4].value()?.into_literal() {
379                LiteralValue::Int(i) => i,
380                LiteralValue::Number(n) => n as i64,
381                _ => 0,
382            }
383        } else {
384            0
385        };
386        let search_mode = if args.len() >= 6 {
387            match args[5].value()?.into_literal() {
388                LiteralValue::Int(i) => i,
389                LiteralValue::Number(n) => n as i64,
390                _ => 1,
391            }
392        } else {
393            1
394        };
395
396        let wildcard = match_mode == 2;
397
398        let mut found: Option<usize> = None;
399        let needle = lookup_value;
400        if match_mode == 0 || wildcard {
401            if search_mode == 1 && lookup_rows > 0 && lookup_cols > 0 {
402                found =
403                    super::lookup_utils::find_exact_index_in_view(&lookup_view, &needle, wildcard)?;
404            } else if search_mode == -1 {
405                for i in (0..lookup_len).rev() {
406                    let cand = if vertical {
407                        lookup_view.get_cell(i, 0)
408                    } else {
409                        lookup_view.get_cell(0, i)
410                    };
411                    if equals_maybe_wildcard(&needle, &cand, wildcard) {
412                        found = Some(i);
413                        break;
414                    }
415                }
416            } else {
417                // Fallback linear scan (also used when the lookup view is empty and
418                // we are treating missing cells as Empty).
419                for i in 0..lookup_len {
420                    let cand = if vertical {
421                        lookup_view.get_cell(i, 0)
422                    } else {
423                        lookup_view.get_cell(0, i)
424                    };
425                    if equals_maybe_wildcard(&needle, &cand, wildcard) {
426                        found = Some(i);
427                        break;
428                    }
429                }
430            }
431        } else if match_mode == -1 || match_mode == 1 {
432            let needle_num = value_to_f64_lenient(&needle);
433            let mut best_idx: Option<usize> = None;
434            let mut best_val: f64 = if match_mode == -1 {
435                f64::NEG_INFINITY
436            } else {
437                f64::INFINITY
438            };
439
440            let mut prev: Option<LiteralValue> = None;
441            for i in 0..lookup_len {
442                let cand = if vertical {
443                    lookup_view.get_cell(i, 0)
444                } else {
445                    lookup_view.get_cell(0, i)
446                };
447
448                if let Some(p) = prev.as_ref() {
449                    let sorted_ok = cmp_for_lookup(p, &cand).is_some_and(|o| o <= 0);
450                    if !sorted_ok {
451                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
452                            ExcelError::new(ExcelErrorKind::Na),
453                        )));
454                    }
455                }
456                prev = Some(cand.clone());
457
458                if cmp_for_lookup(&cand, &needle).is_some_and(|o| o == 0) {
459                    found = Some(i);
460                    break;
461                }
462
463                if let (Some(nn), Some(vv)) = (needle_num, value_to_f64_lenient(&cand)) {
464                    if match_mode == -1 {
465                        if vv <= nn && vv > best_val {
466                            best_val = vv;
467                            best_idx = Some(i);
468                        }
469                    } else if vv >= nn && vv < best_val {
470                        best_val = vv;
471                        best_idx = Some(i);
472                    }
473                }
474            }
475
476            if found.is_none() {
477                found = best_idx;
478            }
479        } else {
480            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
481                ExcelError::new(ExcelErrorKind::Value),
482            )));
483        }
484
485        if let Some(idx) = found {
486            let (ret_rows, ret_cols) = ret_view.dims();
487            if ret_rows == 0 || ret_cols == 0 {
488                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Empty));
489            }
490
491            if vertical {
492                if ret_cols == 1 {
493                    return Ok(crate::traits::CalcValue::Scalar(ret_view.get_cell(idx, 0)));
494                }
495                let mut row_out: Vec<LiteralValue> = Vec::with_capacity(ret_cols);
496                for c in 0..ret_cols {
497                    row_out.push(ret_view.get_cell(idx, c));
498                }
499                return Ok(crate::traits::CalcValue::Range(
500                    crate::engine::range_view::RangeView::from_owned_rows(
501                        vec![row_out],
502                        _ctx.date_system(),
503                    ),
504                ));
505            }
506
507            // Horizontal orientation: treat idx as column.
508            if ret_rows == 1 {
509                return Ok(crate::traits::CalcValue::Scalar(ret_view.get_cell(0, idx)));
510            }
511
512            let mut col_out: Vec<Vec<LiteralValue>> = Vec::with_capacity(ret_rows);
513            for r in 0..ret_rows {
514                col_out.push(vec![ret_view.get_cell(r, idx)]);
515            }
516            return Ok(crate::traits::CalcValue::Range(
517                crate::engine::range_view::RangeView::from_owned_rows(col_out, _ctx.date_system()),
518            ));
519        }
520
521        if args.len() >= 4 {
522            return args[3].value();
523        }
524        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
525            ExcelError::new(ExcelErrorKind::Na),
526        )))
527    }
528}
529
530/* ───────────────────────── XMATCH() ───────────────────────── */
531
532#[derive(Debug)]
533pub struct XMatchFn;
534/// Returns the 1-based position of a value in a one-dimensional lookup array.
535///
536/// `XMATCH` extends `MATCH` with explicit search direction and wildcard mode.
537///
538/// # Remarks
539/// - Defaults: `match_mode=0` (exact), `search_mode=1` (first-to-last).
540/// - `match_mode`: `0` exact, `-1` exact-or-next-smaller, `1` exact-or-next-larger, `2` wildcard.
541/// - `search_mode`: `1` forward, `-1` reverse, `2` ascending binary intent, `-2` descending binary intent.
542/// - `lookup_array` must be a single row or single column, otherwise returns `#VALUE!`.
543/// - Not found returns `#N/A`.
544///
545/// # Examples
546/// ```yaml,sandbox
547/// title: "Exact match position"
548/// grid:
549///   A1: "alpha"
550///   A2: "beta"
551///   A3: "gamma"
552/// formula: '=XMATCH("beta",A1:A3)'
553/// expected: 2
554/// ```
555///
556/// ```yaml,sandbox
557/// title: "Reverse search finds last duplicate"
558/// grid:
559///   A1: 7
560///   A2: 9
561///   A3: 7
562/// formula: '=XMATCH(7,A1:A3,0,-1)'
563/// expected: 3
564/// ```
565///
566/// ```yaml,docs
567/// related:
568///   - XLOOKUP
569///   - MATCH
570///   - INDEX
571/// faq:
572///   - q: "How do search_mode values affect duplicate matches?"
573///     a: "search_mode=1 returns the first qualifying match, while search_mode=-1 scans from the end and returns the last qualifying match."
574///   - q: "When do binary-intent search modes (2 or -2) return #N/A?"
575///     a: "For approximate modes they require sorted data in the expected direction; unsorted arrays are treated as no valid match and return #N/A."
576/// ```
577/// [formualizer-docgen:schema:start]
578/// Name: XMATCH
579/// Type: XMatchFn
580/// Min args: 2
581/// Max args: variadic
582/// Variadic: true
583/// Signature: XMATCH(arg1: any@scalar, arg2: range@range, arg3?: number@scalar, arg4?...: number@scalar)
584/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg3{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg4{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}
585/// Caps: PURE, LOOKUP
586/// [formualizer-docgen:schema:end]
587impl Function for XMatchFn {
588    func_caps!(PURE, LOOKUP);
589    fn name(&self) -> &'static str {
590        "XMATCH"
591    }
592    fn min_args(&self) -> usize {
593        2
594    }
595    fn variadic(&self) -> bool {
596        true
597    }
598    fn arg_schema(&self) -> &'static [ArgSchema] {
599        use once_cell::sync::Lazy;
600        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
601            vec![
602                // lookup_value
603                ArgSchema {
604                    kinds: smallvec::smallvec![ArgKind::Any],
605                    required: true,
606                    by_ref: false,
607                    shape: ShapeKind::Scalar,
608                    coercion: CoercionPolicy::None,
609                    max: None,
610                    repeating: None,
611                    default: None,
612                },
613                // lookup_array (range)
614                ArgSchema {
615                    kinds: smallvec::smallvec![ArgKind::Range],
616                    required: true,
617                    by_ref: true,
618                    shape: ShapeKind::Range,
619                    coercion: CoercionPolicy::None,
620                    max: None,
621                    repeating: None,
622                    default: None,
623                },
624                // match_mode (number) default 0
625                // 0 = exact (default), -1 = exact or next smaller, 1 = exact or next larger, 2 = wildcard
626                ArgSchema {
627                    kinds: smallvec::smallvec![ArgKind::Number],
628                    required: false,
629                    by_ref: false,
630                    shape: ShapeKind::Scalar,
631                    coercion: CoercionPolicy::NumberLenientText,
632                    max: None,
633                    repeating: None,
634                    default: Some(LiteralValue::Int(0)),
635                },
636                // search_mode (number) default 1
637                // 1 = first to last (default), -1 = last to first, 2 = binary ascending, -2 = binary descending
638                ArgSchema {
639                    kinds: smallvec::smallvec![ArgKind::Number],
640                    required: false,
641                    by_ref: false,
642                    shape: ShapeKind::Scalar,
643                    coercion: CoercionPolicy::NumberLenientText,
644                    max: None,
645                    repeating: None,
646                    default: Some(LiteralValue::Int(1)),
647                },
648            ]
649        });
650        &SCHEMA
651    }
652    fn eval<'a, 'b, 'c>(
653        &self,
654        args: &'c [ArgumentHandle<'a, 'b>],
655        _ctx: &dyn FunctionContext<'b>,
656    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
657        if args.len() < 2 {
658            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
659                ExcelError::new(ExcelErrorKind::Value),
660            )));
661        }
662        let lookup_value = args[0].value()?.into_literal();
663        if let LiteralValue::Error(ref e) = lookup_value {
664            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
665                e.clone(),
666            )));
667        }
668        let lookup_view = match args[1].range_view() {
669            Ok(v) => v,
670            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
671        };
672
673        let (lookup_rows, lookup_cols) = lookup_view.dims();
674
675        // XMATCH requires a 1-D lookup array (single row or single column).
676        let vertical = if lookup_cols == 1 {
677            true
678        } else if lookup_rows == 1 {
679            false
680        } else if lookup_rows == 0 || lookup_cols == 0 {
681            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
682                ExcelError::new(ExcelErrorKind::Na),
683            )));
684        } else {
685            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
686                ExcelError::new(ExcelErrorKind::Value),
687            )));
688        };
689
690        let lookup_len = if vertical { lookup_rows } else { lookup_cols };
691
692        if lookup_len == 0 {
693            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
694                ExcelError::new(ExcelErrorKind::Na),
695            )));
696        }
697
698        let match_mode = if args.len() >= 3 {
699            match args[2].value()?.into_literal() {
700                LiteralValue::Int(i) => i,
701                LiteralValue::Number(n) => n as i64,
702                _ => 0,
703            }
704        } else {
705            0
706        };
707        let search_mode = if args.len() >= 4 {
708            match args[3].value()?.into_literal() {
709                LiteralValue::Int(i) => i,
710                LiteralValue::Number(n) => n as i64,
711                _ => 1,
712            }
713        } else {
714            1
715        };
716
717        let wildcard = match_mode == 2;
718        let needle = lookup_value;
719
720        let mut found: Option<usize> = None;
721
722        if match_mode == 0 || wildcard {
723            // Exact match or wildcard match
724            if search_mode == 1 || search_mode == 2 {
725                // Forward search (first to last) or binary ascending (treated as forward for exact)
726                if lookup_rows > 0 && lookup_cols > 0 {
727                    found = super::lookup_utils::find_exact_index_in_view(
728                        &lookup_view,
729                        &needle,
730                        wildcard,
731                    )?;
732                }
733            } else if search_mode == -1 || search_mode == -2 {
734                // Reverse search (last to first) or binary descending (treated as reverse for exact)
735                for i in (0..lookup_len).rev() {
736                    let cand = if vertical {
737                        lookup_view.get_cell(i, 0)
738                    } else {
739                        lookup_view.get_cell(0, i)
740                    };
741                    if equals_maybe_wildcard(&needle, &cand, wildcard) {
742                        found = Some(i);
743                        break;
744                    }
745                }
746            } else {
747                // Fallback linear scan
748                for i in 0..lookup_len {
749                    let cand = if vertical {
750                        lookup_view.get_cell(i, 0)
751                    } else {
752                        lookup_view.get_cell(0, i)
753                    };
754                    if equals_maybe_wildcard(&needle, &cand, wildcard) {
755                        found = Some(i);
756                        break;
757                    }
758                }
759            }
760        } else if match_mode == -1 || match_mode == 1 {
761            // Approximate match: -1 = exact or next smaller, 1 = exact or next larger
762            let needle_num = value_to_f64_lenient(&needle);
763            let mut best_idx: Option<usize> = None;
764            let mut best_val: f64 = if match_mode == -1 {
765                f64::NEG_INFINITY
766            } else {
767                f64::INFINITY
768            };
769
770            // Determine iteration direction based on search_mode
771            let use_reverse = search_mode == -1 || search_mode == -2;
772            let indices: Box<dyn Iterator<Item = usize>> = if use_reverse {
773                Box::new((0..lookup_len).rev())
774            } else {
775                Box::new(0..lookup_len)
776            };
777
778            // For binary search modes (2, -2), data should be sorted
779            // We verify sorting for approximate modes
780            if (search_mode == 2 || search_mode == -2) && match_mode != 0 {
781                let ascending = search_mode == 2;
782                let mut prev: Option<LiteralValue> = None;
783                for i in 0..lookup_len {
784                    let cand = if vertical {
785                        lookup_view.get_cell(i, 0)
786                    } else {
787                        lookup_view.get_cell(0, i)
788                    };
789                    if let Some(p) = prev.as_ref() {
790                        let sorted_ok = if ascending {
791                            cmp_for_lookup(p, &cand).is_some_and(|o| o <= 0)
792                        } else {
793                            cmp_for_lookup(p, &cand).is_some_and(|o| o >= 0)
794                        };
795                        if !sorted_ok {
796                            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
797                                ExcelError::new(ExcelErrorKind::Na),
798                            )));
799                        }
800                    }
801                    prev = Some(cand);
802                }
803            }
804
805            for i in indices {
806                let cand = if vertical {
807                    lookup_view.get_cell(i, 0)
808                } else {
809                    lookup_view.get_cell(0, i)
810                };
811
812                if cmp_for_lookup(&cand, &needle).is_some_and(|o| o == 0) {
813                    found = Some(i);
814                    break;
815                }
816
817                if let (Some(nn), Some(vv)) = (needle_num, value_to_f64_lenient(&cand)) {
818                    if match_mode == -1 {
819                        // exact or next smaller
820                        if vv <= nn && vv > best_val {
821                            best_val = vv;
822                            best_idx = Some(i);
823                        }
824                    } else {
825                        // match_mode == 1: exact or next larger
826                        if vv >= nn && vv < best_val {
827                            best_val = vv;
828                            best_idx = Some(i);
829                        }
830                    }
831                }
832            }
833
834            if found.is_none() {
835                found = best_idx;
836            }
837        } else {
838            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
839                ExcelError::new(ExcelErrorKind::Value),
840            )));
841        }
842
843        match found {
844            Some(idx) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
845                (idx + 1) as i64,
846            ))),
847            None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
848                ExcelError::new(ExcelErrorKind::Na),
849            ))),
850        }
851    }
852}
853
854/* ───────────────────────── SORT() ───────────────────────── */
855
856#[derive(Debug)]
857pub struct SortFn;
858/// Sorts an array by a selected row or column and returns a spilled result.
859///
860/// `SORT` can order rows (default) or columns.
861///
862/// # Remarks
863/// - Defaults: `sort_index=1`, `sort_order=1` (ascending), `by_col=FALSE`.
864/// - `sort_index` is 1-based in the active sort axis.
865/// - `sort_order < 0` sorts descending; otherwise ascending.
866/// - Invalid sort indexes return `#VALUE!`.
867/// - Empty input returns an empty spill.
868///
869/// # Examples
870/// ```yaml,sandbox
871/// title: "Sort rows by second column"
872/// grid:
873///   A1: "C"
874///   B1: 30
875///   A2: "A"
876///   B2: 10
877///   A3: "B"
878///   B3: 20
879/// formula: '=SORT(A1:B3,2,1,FALSE)'
880/// expected: [["A",10],["B",20],["C",30]]
881/// ```
882///
883/// ```yaml,sandbox
884/// title: "Sort columns by first row descending"
885/// grid:
886///   A1: 1
887///   B1: 3
888///   C1: 2
889///   A2: "A"
890///   B2: "C"
891///   C2: "B"
892/// formula: '=SORT(A1:C2,1,-1,TRUE)'
893/// expected: [[3,2,1],["C","B","A"]]
894/// ```
895///
896/// ```yaml,docs
897/// related:
898///   - SORTBY
899///   - TAKE
900///   - DROP
901/// faq:
902///   - q: "What changes when by_col is TRUE?"
903///     a: "SORT reorders columns instead of rows, and sort_index is interpreted as a row index used as the sort key."
904///   - q: "What causes #VALUE! in SORT?"
905///     a: "If sort_index is outside the active axis (row or column axis based on by_col), SORT returns #VALUE!."
906/// ```
907/// [formualizer-docgen:schema:start]
908/// Name: SORT
909/// Type: SortFn
910/// Min args: 1
911/// Max args: variadic
912/// Variadic: true
913/// Signature: SORT(arg1: range@range, arg2?: number@scalar, arg3?: number@scalar, arg4?...: logical@scalar)
914/// Arg schema: arg1{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg3{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg4{kinds=logical,required=false,shape=scalar,by_ref=false,coercion=Logical,max=None,repeating=None,default=true}
915/// Caps: PURE
916/// [formualizer-docgen:schema:end]
917impl Function for SortFn {
918    func_caps!(PURE);
919    fn name(&self) -> &'static str {
920        "SORT"
921    }
922    fn min_args(&self) -> usize {
923        1
924    }
925    fn variadic(&self) -> bool {
926        true
927    }
928    fn arg_schema(&self) -> &'static [ArgSchema] {
929        use once_cell::sync::Lazy;
930        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
931            vec![
932                // array
933                ArgSchema {
934                    kinds: smallvec::smallvec![ArgKind::Range],
935                    required: true,
936                    by_ref: true,
937                    shape: ShapeKind::Range,
938                    coercion: CoercionPolicy::None,
939                    max: None,
940                    repeating: None,
941                    default: None,
942                },
943                // sort_index (default 1)
944                ArgSchema {
945                    kinds: smallvec::smallvec![ArgKind::Number],
946                    required: false,
947                    by_ref: false,
948                    shape: ShapeKind::Scalar,
949                    coercion: CoercionPolicy::NumberLenientText,
950                    max: None,
951                    repeating: None,
952                    default: Some(LiteralValue::Int(1)),
953                },
954                // sort_order (default 1 = ascending, -1 = descending)
955                ArgSchema {
956                    kinds: smallvec::smallvec![ArgKind::Number],
957                    required: false,
958                    by_ref: false,
959                    shape: ShapeKind::Scalar,
960                    coercion: CoercionPolicy::NumberLenientText,
961                    max: None,
962                    repeating: None,
963                    default: Some(LiteralValue::Int(1)),
964                },
965                // by_col (default FALSE = sort rows, TRUE = sort columns)
966                ArgSchema {
967                    kinds: smallvec::smallvec![ArgKind::Logical],
968                    required: false,
969                    by_ref: false,
970                    shape: ShapeKind::Scalar,
971                    coercion: CoercionPolicy::Logical,
972                    max: None,
973                    repeating: None,
974                    default: Some(LiteralValue::Boolean(false)),
975                },
976            ]
977        });
978        &SCHEMA
979    }
980    fn eval<'a, 'b, 'c>(
981        &self,
982        args: &'c [ArgumentHandle<'a, 'b>],
983        _ctx: &dyn FunctionContext<'b>,
984    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
985        let view = match args[0].range_view() {
986            Ok(v) => v,
987            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
988        };
989        let (rows, cols) = view.dims();
990        if rows == 0 || cols == 0 {
991            return Ok(crate::traits::CalcValue::Range(
992                crate::engine::range_view::RangeView::from_owned_rows(vec![], _ctx.date_system()),
993            ));
994        }
995
996        let sort_index = if args.len() >= 2 {
997            match args[1].value()?.into_literal() {
998                LiteralValue::Int(i) => i,
999                LiteralValue::Number(n) => n as i64,
1000                _ => 1,
1001            }
1002        } else {
1003            1
1004        };
1005
1006        let sort_order = if args.len() >= 3 {
1007            match args[2].value()?.into_literal() {
1008                LiteralValue::Int(i) => i,
1009                LiteralValue::Number(n) => n as i64,
1010                _ => 1,
1011            }
1012        } else {
1013            1
1014        };
1015
1016        let by_col = if args.len() >= 4 {
1017            matches!(args[3].value()?.into_literal(), LiteralValue::Boolean(true))
1018        } else {
1019            false
1020        };
1021
1022        let ascending = sort_order >= 0;
1023
1024        if by_col {
1025            // Sort columns by the specified row
1026            let sort_row_idx = (sort_index - 1).max(0) as usize;
1027            if sort_row_idx >= rows {
1028                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1029                    ExcelError::new(ExcelErrorKind::Value),
1030                )));
1031            }
1032
1033            // Extract columns as vectors
1034            let mut columns: Vec<(usize, Vec<LiteralValue>)> = Vec::with_capacity(cols);
1035            for c in 0..cols {
1036                let mut col_vals: Vec<LiteralValue> = Vec::with_capacity(rows);
1037                for r in 0..rows {
1038                    col_vals.push(view.get_cell(r, c));
1039                }
1040                columns.push((c, col_vals));
1041            }
1042
1043            // Sort columns by the value in sort_row_idx
1044            columns.sort_by(|a, b| {
1045                let val_a = &a.1[sort_row_idx];
1046                let val_b = &b.1[sort_row_idx];
1047                let cmp = cmp_for_lookup(val_a, val_b).unwrap_or(0);
1048                if ascending { cmp.cmp(&0) } else { 0.cmp(&cmp) }
1049            });
1050
1051            // Reconstruct the array with sorted columns
1052            let mut out: Vec<Vec<LiteralValue>> = vec![Vec::with_capacity(cols); rows];
1053            for (_orig_idx, col_vals) in columns {
1054                for (r, val) in col_vals.into_iter().enumerate() {
1055                    out[r].push(val);
1056                }
1057            }
1058
1059            Ok(collapse_if_scalar(out, _ctx.date_system()))
1060        } else {
1061            // Sort rows by the specified column
1062            let sort_col_idx = (sort_index - 1).max(0) as usize;
1063            if sort_col_idx >= cols {
1064                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1065                    ExcelError::new(ExcelErrorKind::Value),
1066                )));
1067            }
1068
1069            // Extract rows
1070            let mut row_data: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows);
1071            for r in 0..rows {
1072                let mut row_vals: Vec<LiteralValue> = Vec::with_capacity(cols);
1073                for c in 0..cols {
1074                    row_vals.push(view.get_cell(r, c));
1075                }
1076                row_data.push(row_vals);
1077            }
1078
1079            // Sort rows by the value in sort_col_idx
1080            row_data.sort_by(|a, b| {
1081                let val_a = &a[sort_col_idx];
1082                let val_b = &b[sort_col_idx];
1083                let cmp = cmp_for_lookup(val_a, val_b).unwrap_or(0);
1084                if ascending { cmp.cmp(&0) } else { 0.cmp(&cmp) }
1085            });
1086
1087            Ok(collapse_if_scalar(row_data, _ctx.date_system()))
1088        }
1089    }
1090}
1091
1092/* ───────────────────────── SORTBY() ───────────────────────── */
1093
1094#[derive(Debug)]
1095pub struct SortByFn;
1096/// Sorts an array based on one or more aligned sort-by arrays.
1097///
1098/// `SORTBY` separates what is returned (`array`) from what determines ordering (`by_array`).
1099///
1100/// # Remarks
1101/// - Requires at least one `by_array` aligned to the row count of `array`.
1102/// - `sort_order` defaults to ascending when omitted.
1103/// - Additional `by_array`/`sort_order` criteria are processed left-to-right.
1104/// - Shape mismatches or invalid criteria return `#VALUE!`.
1105/// - Returns a spilled sorted array.
1106///
1107/// # Examples
1108/// ```yaml,sandbox
1109/// title: "Sort names by score"
1110/// grid:
1111///   A1: "Charlie"
1112///   A2: "Alice"
1113///   A3: "Bob"
1114///   B1: 3
1115///   B2: 1
1116///   B3: 2
1117/// formula: '=SORTBY(A1:A3,B1:B3)'
1118/// expected: [["Alice"],["Bob"],["Charlie"]]
1119/// ```
1120///
1121/// ```yaml,sandbox
1122/// title: "Sort descending by key"
1123/// grid:
1124///   A1: "Q1"
1125///   A2: "Q2"
1126///   A3: "Q3"
1127///   B1: 100
1128///   B2: 300
1129///   B3: 200
1130/// formula: '=SORTBY(A1:A3,B1:B3,-1)'
1131/// expected: [["Q2"],["Q3"],["Q1"]]
1132/// ```
1133///
1134/// ```yaml,docs
1135/// related:
1136///   - SORT
1137///   - UNIQUE
1138///   - FILTER
1139/// faq:
1140///   - q: "How are multiple sort criteria applied?"
1141///     a: "SORTBY evaluates criteria left-to-right, using later by_array values only when earlier criteria compare equal."
1142///   - q: "Why do I get #VALUE! with SORTBY?"
1143///     a: "Each by_array must be one-dimensional and aligned to the primary array row count; mismatched shapes return #VALUE!."
1144/// ```
1145/// [formualizer-docgen:schema:start]
1146/// Name: SORTBY
1147/// Type: SortByFn
1148/// Min args: 2
1149/// Max args: variadic
1150/// Variadic: true
1151/// Signature: SORTBY(arg1: range@range, arg2: range@range, arg3?...: number@scalar)
1152/// Arg schema: arg1{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg3{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}
1153/// Caps: PURE
1154/// [formualizer-docgen:schema:end]
1155impl Function for SortByFn {
1156    func_caps!(PURE);
1157    fn name(&self) -> &'static str {
1158        "SORTBY"
1159    }
1160    fn min_args(&self) -> usize {
1161        2
1162    }
1163    fn variadic(&self) -> bool {
1164        true
1165    }
1166    fn arg_schema(&self) -> &'static [ArgSchema] {
1167        use once_cell::sync::Lazy;
1168        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
1169            vec![
1170                // array
1171                ArgSchema {
1172                    kinds: smallvec::smallvec![ArgKind::Range],
1173                    required: true,
1174                    by_ref: true,
1175                    shape: ShapeKind::Range,
1176                    coercion: CoercionPolicy::None,
1177                    max: None,
1178                    repeating: None,
1179                    default: None,
1180                },
1181                // by_array1
1182                ArgSchema {
1183                    kinds: smallvec::smallvec![ArgKind::Range],
1184                    required: true,
1185                    by_ref: true,
1186                    shape: ShapeKind::Range,
1187                    coercion: CoercionPolicy::None,
1188                    max: None,
1189                    repeating: None,
1190                    default: None,
1191                },
1192                // sort_order1 (optional, default 1)
1193                ArgSchema {
1194                    kinds: smallvec::smallvec![ArgKind::Number],
1195                    required: false,
1196                    by_ref: false,
1197                    shape: ShapeKind::Scalar,
1198                    coercion: CoercionPolicy::NumberLenientText,
1199                    max: None,
1200                    repeating: None,
1201                    default: Some(LiteralValue::Int(1)),
1202                },
1203                // Additional by_array/sort_order pairs can follow (variadic)
1204            ]
1205        });
1206        &SCHEMA
1207    }
1208    fn eval<'a, 'b, 'c>(
1209        &self,
1210        args: &'c [ArgumentHandle<'a, 'b>],
1211        _ctx: &dyn FunctionContext<'b>,
1212    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1213        if args.len() < 2 {
1214            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1215                ExcelError::new(ExcelErrorKind::Value),
1216            )));
1217        }
1218
1219        let view = match args[0].range_view() {
1220            Ok(v) => v,
1221            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1222        };
1223        let (rows, cols) = view.dims();
1224        if rows == 0 || cols == 0 {
1225            return Ok(crate::traits::CalcValue::Range(
1226                crate::engine::range_view::RangeView::from_owned_rows(vec![], _ctx.date_system()),
1227            ));
1228        }
1229
1230        // Parse sort criteria: pairs of (by_array, sort_order)
1231        // Arguments after array: by_array1, [sort_order1], [by_array2], [sort_order2], ...
1232        let mut sort_criteria: Vec<(Vec<LiteralValue>, bool)> = Vec::new();
1233        let mut arg_idx = 1;
1234
1235        while arg_idx < args.len() {
1236            // by_array
1237            let by_view = match args[arg_idx].range_view() {
1238                Ok(v) => v,
1239                Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1240            };
1241            let (by_rows, by_cols) = by_view.dims();
1242
1243            // The by_array should be 1-D and match the number of rows in the main array
1244            let by_values: Vec<LiteralValue> = if by_cols == 1 {
1245                if by_rows != rows {
1246                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1247                        ExcelError::new(ExcelErrorKind::Value),
1248                    )));
1249                }
1250                (0..by_rows).map(|r| by_view.get_cell(r, 0)).collect()
1251            } else if by_rows == 1 {
1252                if by_cols != rows {
1253                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1254                        ExcelError::new(ExcelErrorKind::Value),
1255                    )));
1256                }
1257                (0..by_cols).map(|c| by_view.get_cell(0, c)).collect()
1258            } else {
1259                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1260                    ExcelError::new(ExcelErrorKind::Value),
1261                )));
1262            };
1263
1264            arg_idx += 1;
1265
1266            // sort_order (optional)
1267            let ascending = if arg_idx < args.len() {
1268                // TODO(phase6): SORTBY parsing can mis-handle multi-criteria sort_order.
1269                // Check if next arg is a number (sort_order) or a range (next by_array)
1270                match args[arg_idx].value() {
1271                    Ok(v) => {
1272                        let lit = v.into_literal();
1273                        match lit {
1274                            LiteralValue::Int(i) => {
1275                                arg_idx += 1;
1276                                i >= 0
1277                            }
1278                            LiteralValue::Number(n) => {
1279                                arg_idx += 1;
1280                                n >= 0.0
1281                            }
1282                            _ => true, // Next arg is likely a range, use default ascending
1283                        }
1284                    }
1285                    Err(_) => true,
1286                }
1287            } else {
1288                true
1289            };
1290
1291            sort_criteria.push((by_values, ascending));
1292        }
1293
1294        if sort_criteria.is_empty() {
1295            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1296                ExcelError::new(ExcelErrorKind::Value),
1297            )));
1298        }
1299
1300        // Extract rows with their indices
1301        let mut indexed_rows: Vec<(usize, Vec<LiteralValue>)> = Vec::with_capacity(rows);
1302        for r in 0..rows {
1303            let mut row_vals: Vec<LiteralValue> = Vec::with_capacity(cols);
1304            for c in 0..cols {
1305                row_vals.push(view.get_cell(r, c));
1306            }
1307            indexed_rows.push((r, row_vals));
1308        }
1309
1310        // Sort using all criteria
1311        indexed_rows.sort_by(|a, b| {
1312            for (by_values, ascending) in &sort_criteria {
1313                let val_a = &by_values[a.0];
1314                let val_b = &by_values[b.0];
1315                let cmp = cmp_for_lookup(val_a, val_b).unwrap_or(0);
1316                if cmp != 0 {
1317                    return if *ascending { cmp.cmp(&0) } else { 0.cmp(&cmp) };
1318                }
1319            }
1320            std::cmp::Ordering::Equal
1321        });
1322
1323        // Extract sorted rows
1324        let out: Vec<Vec<LiteralValue>> = indexed_rows.into_iter().map(|(_, row)| row).collect();
1325
1326        Ok(collapse_if_scalar(out, _ctx.date_system()))
1327    }
1328}
1329
1330/* ───────────────────────── RANDARRAY() ───────────────────────── */
1331
1332#[derive(Debug)]
1333pub struct RandArrayFn;
1334/// Generates a random spilled array of numbers.
1335///
1336/// `RANDARRAY` can return decimal values or whole numbers in a specified range.
1337///
1338/// # Remarks
1339/// - Defaults: `rows=1`, `columns=1`, `min=0`, `max=1`, `whole_number=FALSE`.
1340/// - The function is non-deterministic and recalculates to new values.
1341/// - For whole numbers, values are generated in an inclusive integer range.
1342/// - `rows <= 0`, `columns <= 0`, or invalid integer bounds return `#VALUE!`.
1343///
1344/// # Examples
1345/// ```yaml,sandbox
1346/// title: "Generate a 2x3 decimal matrix"
1347/// formula: '=RANDARRAY(2,3)'
1348/// expected: "2x3 array of decimals in [0,1)"
1349/// ```
1350///
1351/// ```yaml,sandbox
1352/// title: "Generate six integer dice rolls"
1353/// formula: '=RANDARRAY(6,1,1,6,TRUE)'
1354/// expected: "6x1 array of integers from 1 to 6"
1355/// ```
1356///
1357/// ```yaml,docs
1358/// related:
1359///   - SEQUENCE
1360///   - SORT
1361///   - UNIQUE
1362/// faq:
1363///   - q: "Are RANDARRAY bounds inclusive?"
1364///     a: "In whole_number mode, min and max are inclusive integer bounds; in decimal mode values are generated over the numeric interval from min toward max."
1365///   - q: "Why does RANDARRAY recalculate on every recalc pass?"
1366///     a: "RANDARRAY is volatile and non-deterministic by design, so its spilled results are regenerated each evaluation."
1367/// ```
1368/// [formualizer-docgen:schema:start]
1369/// Name: RANDARRAY
1370/// Type: RandArrayFn
1371/// Min args: 0
1372/// Max args: variadic
1373/// Variadic: true
1374/// Signature: RANDARRAY(arg1?: number@scalar, arg2?: number@scalar, arg3?: number@scalar, arg4?: number@scalar, arg5?...: logical@scalar)
1375/// Arg schema: arg1{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg2{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg3{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg4{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg5{kinds=logical,required=false,shape=scalar,by_ref=false,coercion=Logical,max=None,repeating=None,default=true}
1376/// Caps: none
1377/// [formualizer-docgen:schema:end]
1378impl Function for RandArrayFn {
1379    // Note: RANDARRAY is NOT pure - it returns different values on each evaluation
1380    fn caps(&self) -> crate::function::FnCaps {
1381        crate::function::FnCaps::empty()
1382    }
1383    fn name(&self) -> &'static str {
1384        "RANDARRAY"
1385    }
1386    fn min_args(&self) -> usize {
1387        0
1388    }
1389    fn variadic(&self) -> bool {
1390        true
1391    }
1392    fn arg_schema(&self) -> &'static [ArgSchema] {
1393        use once_cell::sync::Lazy;
1394        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
1395            vec![
1396                // rows (default 1)
1397                ArgSchema {
1398                    kinds: smallvec::smallvec![ArgKind::Number],
1399                    required: false,
1400                    by_ref: false,
1401                    shape: ShapeKind::Scalar,
1402                    coercion: CoercionPolicy::NumberLenientText,
1403                    max: None,
1404                    repeating: None,
1405                    default: Some(LiteralValue::Int(1)),
1406                },
1407                // columns (default 1)
1408                ArgSchema {
1409                    kinds: smallvec::smallvec![ArgKind::Number],
1410                    required: false,
1411                    by_ref: false,
1412                    shape: ShapeKind::Scalar,
1413                    coercion: CoercionPolicy::NumberLenientText,
1414                    max: None,
1415                    repeating: None,
1416                    default: Some(LiteralValue::Int(1)),
1417                },
1418                // min (default 0)
1419                ArgSchema {
1420                    kinds: smallvec::smallvec![ArgKind::Number],
1421                    required: false,
1422                    by_ref: false,
1423                    shape: ShapeKind::Scalar,
1424                    coercion: CoercionPolicy::NumberLenientText,
1425                    max: None,
1426                    repeating: None,
1427                    default: Some(LiteralValue::Int(0)),
1428                },
1429                // max (default 1)
1430                ArgSchema {
1431                    kinds: smallvec::smallvec![ArgKind::Number],
1432                    required: false,
1433                    by_ref: false,
1434                    shape: ShapeKind::Scalar,
1435                    coercion: CoercionPolicy::NumberLenientText,
1436                    max: None,
1437                    repeating: None,
1438                    default: Some(LiteralValue::Int(1)),
1439                },
1440                // whole_number (default FALSE)
1441                ArgSchema {
1442                    kinds: smallvec::smallvec![ArgKind::Logical],
1443                    required: false,
1444                    by_ref: false,
1445                    shape: ShapeKind::Scalar,
1446                    coercion: CoercionPolicy::Logical,
1447                    max: None,
1448                    repeating: None,
1449                    default: Some(LiteralValue::Boolean(false)),
1450                },
1451            ]
1452        });
1453        &SCHEMA
1454    }
1455    fn eval<'a, 'b, 'c>(
1456        &self,
1457        args: &'c [ArgumentHandle<'a, 'b>],
1458        _ctx: &dyn FunctionContext<'b>,
1459    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1460        use rand::Rng;
1461
1462        // Extract numbers (allow float but coerce to i64 for dimensions)
1463        let num = |a: &ArgumentHandle| -> Result<f64, ExcelError> {
1464            Ok(match a.value()?.into_literal() {
1465                LiteralValue::Int(i) => i as f64,
1466                LiteralValue::Number(n) => n,
1467                LiteralValue::Error(e) => return Err(e),
1468                _other => {
1469                    return Err(ExcelError::new(ExcelErrorKind::Value));
1470                }
1471            })
1472        };
1473
1474        let rows = if !args.is_empty() {
1475            num(&args[0])? as i64
1476        } else {
1477            1
1478        };
1479        let cols = if args.len() >= 2 {
1480            num(&args[1])? as i64
1481        } else {
1482            1
1483        };
1484        let min_val = if args.len() >= 3 { num(&args[2])? } else { 0.0 };
1485        let max_val = if args.len() >= 4 { num(&args[3])? } else { 1.0 };
1486        let whole_number = if args.len() >= 5 {
1487            matches!(args[4].value()?.into_literal(), LiteralValue::Boolean(true))
1488        } else {
1489            false
1490        };
1491
1492        // Validate dimensions
1493        if rows <= 0 || cols <= 0 {
1494            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1495                ExcelError::new(ExcelErrorKind::Value),
1496            )));
1497        }
1498
1499        // Validate min <= max for whole numbers
1500        if whole_number && min_val > max_val {
1501            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1502                ExcelError::new(ExcelErrorKind::Value),
1503            )));
1504        }
1505
1506        let mut rng = rand::thread_rng();
1507        let mut out: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows as usize);
1508
1509        for _r in 0..rows {
1510            let mut row_vec: Vec<LiteralValue> = Vec::with_capacity(cols as usize);
1511            for _c in 0..cols {
1512                let value = if whole_number {
1513                    // Generate random integer in range [min, max] inclusive
1514                    let min_int = min_val.ceil() as i64;
1515                    let max_int = max_val.floor() as i64;
1516                    if min_int > max_int {
1517                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1518                            ExcelError::new(ExcelErrorKind::Value),
1519                        )));
1520                    }
1521                    let rand_int = rng.gen_range(min_int..=max_int);
1522                    LiteralValue::Int(rand_int)
1523                } else {
1524                    // Generate random float in range [min, max)
1525                    let rand_float = rng.r#gen::<f64>() * (max_val - min_val) + min_val;
1526                    LiteralValue::Number(rand_float)
1527                };
1528                row_vec.push(value);
1529            }
1530            out.push(row_vec);
1531        }
1532
1533        Ok(collapse_if_scalar(out, _ctx.date_system()))
1534    }
1535}
1536
1537/* ───────────────────────── GROUPBY() ───────────────────────── */
1538
1539/// Aggregation type for GROUPBY and PIVOTBY
1540#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1541enum GroupAggregation {
1542    Sum,
1543    Average,
1544    Count,
1545    CountA,
1546    Max,
1547    Min,
1548    Product,
1549    StDev,
1550    StDevP,
1551    Var,
1552    VarP,
1553    Median,
1554}
1555
1556impl GroupAggregation {
1557    fn from_literal(val: &LiteralValue) -> Option<Self> {
1558        match val {
1559            LiteralValue::Text(s) => Self::from_str(s),
1560            LiteralValue::Int(n) => Self::from_num(*n as i32),
1561            LiteralValue::Number(n) => Self::from_num(*n as i32),
1562            _ => None,
1563        }
1564    }
1565
1566    fn from_str(s: &str) -> Option<Self> {
1567        let upper = s.to_ascii_uppercase();
1568        match upper.as_str() {
1569            "SUM" => Some(Self::Sum),
1570            "AVERAGE" | "AVG" => Some(Self::Average),
1571            "COUNT" => Some(Self::Count),
1572            "COUNTA" => Some(Self::CountA),
1573            "MAX" => Some(Self::Max),
1574            "MIN" => Some(Self::Min),
1575            "PRODUCT" => Some(Self::Product),
1576            "STDEV" | "STDEV.S" => Some(Self::StDev),
1577            "STDEVP" | "STDEV.P" => Some(Self::StDevP),
1578            "VAR" | "VAR.S" => Some(Self::Var),
1579            "VARP" | "VAR.P" => Some(Self::VarP),
1580            "MEDIAN" => Some(Self::Median),
1581            _ => None,
1582        }
1583    }
1584
1585    fn from_num(n: i32) -> Option<Self> {
1586        // Excel's AGGREGATE function_num mapping (common subset)
1587        match n {
1588            1 => Some(Self::Average),
1589            2 => Some(Self::Count),
1590            3 => Some(Self::CountA),
1591            4 => Some(Self::Max),
1592            5 => Some(Self::Min),
1593            6 => Some(Self::Product),
1594            7 => Some(Self::StDev),
1595            8 => Some(Self::StDevP),
1596            9 => Some(Self::Sum),
1597            10 => Some(Self::Var),
1598            11 => Some(Self::VarP),
1599            12 => Some(Self::Median),
1600            _ => None,
1601        }
1602    }
1603
1604    fn apply(&self, values: &[f64]) -> f64 {
1605        if values.is_empty() {
1606            return match self {
1607                Self::Count | Self::CountA => 0.0,
1608                Self::Sum | Self::Product => 0.0,
1609                _ => f64::NAN,
1610            };
1611        }
1612
1613        match self {
1614            Self::Sum => values.iter().sum(),
1615            Self::Average => values.iter().sum::<f64>() / values.len() as f64,
1616            Self::Count | Self::CountA => values.len() as f64,
1617            Self::Max => values.iter().copied().fold(f64::NEG_INFINITY, f64::max),
1618            Self::Min => values.iter().copied().fold(f64::INFINITY, f64::min),
1619            Self::Product => values.iter().product(),
1620            Self::StDev => {
1621                if values.len() < 2 {
1622                    return f64::NAN;
1623                }
1624                let mean = values.iter().sum::<f64>() / values.len() as f64;
1625                let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>()
1626                    / (values.len() - 1) as f64;
1627                variance.sqrt()
1628            }
1629            Self::StDevP => {
1630                let mean = values.iter().sum::<f64>() / values.len() as f64;
1631                let variance =
1632                    values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
1633                variance.sqrt()
1634            }
1635            Self::Var => {
1636                if values.len() < 2 {
1637                    return f64::NAN;
1638                }
1639                let mean = values.iter().sum::<f64>() / values.len() as f64;
1640                values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64
1641            }
1642            Self::VarP => {
1643                let mean = values.iter().sum::<f64>() / values.len() as f64;
1644                values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64
1645            }
1646            Self::Median => {
1647                let mut sorted = values.to_vec();
1648                sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1649                let mid = sorted.len() / 2;
1650                if sorted.len().is_multiple_of(2) {
1651                    (sorted[mid - 1] + sorted[mid]) / 2.0
1652                } else {
1653                    sorted[mid]
1654                }
1655            }
1656        }
1657    }
1658}
1659
1660/// Helper to convert LiteralValue to a group key string
1661fn literal_to_group_key(v: &LiteralValue) -> String {
1662    match v {
1663        LiteralValue::Text(s) => s.clone(),
1664        LiteralValue::Int(i) => i.to_string(),
1665        LiteralValue::Number(n) => format!("{:.10}", n),
1666        LiteralValue::Boolean(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
1667        LiteralValue::Empty => String::new(),
1668        LiteralValue::Error(e) => format!("#{:?}", e.kind),
1669        LiteralValue::Array(_) => "[Array]".to_string(),
1670        LiteralValue::Date(d) => d.to_string(),
1671        LiteralValue::DateTime(dt) => dt.to_string(),
1672        LiteralValue::Time(t) => t.to_string(),
1673        LiteralValue::Duration(d) => format!("{:?}", d),
1674        LiteralValue::Pending => "[Pending]".to_string(),
1675    }
1676}
1677
1678/// Helper to extract numeric value for aggregation
1679fn literal_to_num_opt(v: &LiteralValue) -> Option<f64> {
1680    match v {
1681        LiteralValue::Number(n) => Some(*n),
1682        LiteralValue::Int(i) => Some(*i as f64),
1683        LiteralValue::Boolean(b) => Some(if *b { 1.0 } else { 0.0 }),
1684        _ => None,
1685    }
1686}
1687
1688#[derive(Debug)]
1689pub struct GroupByFn;
1690
1691/// Groups rows by key fields and returns aggregated values as a spilled table.
1692///
1693/// `GROUPBY` summarizes one or more value columns using a selected aggregation function.
1694///
1695/// # Remarks
1696/// - `row_fields` and `values` must have the same row count.
1697/// - Aggregation can be supplied as text (for example `"SUM"`) or numeric code.
1698/// - Optional controls: `field_headers`, `total_depth`, `sort_order`.
1699/// - Invalid aggregation names/codes return `#VALUE!`.
1700/// - Output is dynamic-array shaped and may include generated headers or totals.
1701///
1702/// # Examples
1703/// ```yaml,sandbox
1704/// title: "Sum sales by region"
1705/// grid:
1706///   A1: "Region"
1707///   A2: "East"
1708///   A3: "West"
1709///   A4: "East"
1710///   B1: "Sales"
1711///   B2: 100
1712///   B3: 80
1713///   B4: 50
1714/// formula: '=GROUPBY(A1:A4,B1:B4,"SUM",3,0,1)'
1715/// expected: [["Region","Sales"],["East",150],["West",80]]
1716/// ```
1717///
1718/// ```yaml,sandbox
1719/// title: "Average with grand total"
1720/// grid:
1721///   A1: "Team"
1722///   A2: "A"
1723///   A3: "A"
1724///   A4: "B"
1725///   B1: "Score"
1726///   B2: 90
1727///   B3: 70
1728///   B4: 80
1729/// formula: '=GROUPBY(A1:A4,B1:B4,"AVERAGE",3,1,1)'
1730/// expected: "Grouped table with team averages plus grand total row"
1731/// ```
1732///
1733/// ```yaml,docs
1734/// related:
1735///   - PIVOTBY
1736///   - SORTBY
1737///   - UNIQUE
1738/// faq:
1739///   - q: "What shape constraints does GROUPBY enforce?"
1740///     a: "row_fields and values must have the same number of rows; mismatched heights return #VALUE!."
1741///   - q: "Can the aggregation function be numeric instead of text?"
1742///     a: "Yes. GROUPBY accepts either function names (like \"SUM\") or numeric function codes, and invalid entries return #VALUE!."
1743/// ```
1744/// [formualizer-docgen:schema:start]
1745/// Name: GROUPBY
1746/// Type: GroupByFn
1747/// Min args: 3
1748/// Max args: variadic
1749/// Variadic: true
1750/// Signature: GROUPBY(arg1: range@range, arg2: range@range, arg3: any@scalar, arg4?: number@scalar, arg5?: number@scalar, arg6?...: number@scalar)
1751/// Arg schema: arg1{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg3{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg4{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg5{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg6{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}
1752/// Caps: PURE
1753/// [formualizer-docgen:schema:end]
1754impl Function for GroupByFn {
1755    func_caps!(PURE);
1756    fn name(&self) -> &'static str {
1757        "GROUPBY"
1758    }
1759    fn min_args(&self) -> usize {
1760        3
1761    }
1762    fn variadic(&self) -> bool {
1763        true
1764    }
1765    fn arg_schema(&self) -> &'static [ArgSchema] {
1766        use once_cell::sync::Lazy;
1767        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
1768            vec![
1769                // row_fields - range to group by
1770                ArgSchema {
1771                    kinds: smallvec::smallvec![ArgKind::Range],
1772                    required: true,
1773                    by_ref: true,
1774                    shape: ShapeKind::Range,
1775                    coercion: CoercionPolicy::None,
1776                    max: None,
1777                    repeating: None,
1778                    default: None,
1779                },
1780                // values - range of values to aggregate
1781                ArgSchema {
1782                    kinds: smallvec::smallvec![ArgKind::Range],
1783                    required: true,
1784                    by_ref: true,
1785                    shape: ShapeKind::Range,
1786                    coercion: CoercionPolicy::None,
1787                    max: None,
1788                    repeating: None,
1789                    default: None,
1790                },
1791                // function - aggregation function (SUM, AVERAGE, etc.)
1792                ArgSchema {
1793                    kinds: smallvec::smallvec![ArgKind::Any],
1794                    required: true,
1795                    by_ref: false,
1796                    shape: ShapeKind::Scalar,
1797                    coercion: CoercionPolicy::None,
1798                    max: None,
1799                    repeating: None,
1800                    default: None,
1801                },
1802                // field_headers (optional) - 0: no headers, 1: has headers (default), 2: generate headers, 3: has headers + generate
1803                ArgSchema {
1804                    kinds: smallvec::smallvec![ArgKind::Number],
1805                    required: false,
1806                    by_ref: false,
1807                    shape: ShapeKind::Scalar,
1808                    coercion: CoercionPolicy::NumberLenientText,
1809                    max: None,
1810                    repeating: None,
1811                    default: Some(LiteralValue::Int(1)),
1812                },
1813                // total_depth (optional) - 0: no totals, 1: grand total, 2: subtotals, etc.
1814                ArgSchema {
1815                    kinds: smallvec::smallvec![ArgKind::Number],
1816                    required: false,
1817                    by_ref: false,
1818                    shape: ShapeKind::Scalar,
1819                    coercion: CoercionPolicy::NumberLenientText,
1820                    max: None,
1821                    repeating: None,
1822                    default: Some(LiteralValue::Int(0)),
1823                },
1824                // sort_order (optional) - 0: no sorting, 1: ascending, -1: descending, 2: by value asc, -2: by value desc
1825                ArgSchema {
1826                    kinds: smallvec::smallvec![ArgKind::Number],
1827                    required: false,
1828                    by_ref: false,
1829                    shape: ShapeKind::Scalar,
1830                    coercion: CoercionPolicy::NumberLenientText,
1831                    max: None,
1832                    repeating: None,
1833                    default: Some(LiteralValue::Int(0)),
1834                },
1835            ]
1836        });
1837        &SCHEMA
1838    }
1839
1840    fn eval<'a, 'b, 'c>(
1841        &self,
1842        args: &'c [ArgumentHandle<'a, 'b>],
1843        _ctx: &dyn FunctionContext<'b>,
1844    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
1845        if args.len() < 3 {
1846            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1847                ExcelError::new(ExcelErrorKind::Value),
1848            )));
1849        }
1850
1851        // Get row_fields and values ranges
1852        let row_fields_view = match args[0].range_view() {
1853            Ok(v) => v,
1854            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1855        };
1856        let values_view = match args[1].range_view() {
1857            Ok(v) => v,
1858            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1859        };
1860
1861        // Parse aggregation function
1862        let agg_val = args[2].value()?.into_literal();
1863        let aggregation = match GroupAggregation::from_literal(&agg_val) {
1864            Some(a) => a,
1865            None => {
1866                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1867                    ExcelError::new(ExcelErrorKind::Value)
1868                        .with_message("Invalid aggregation function"),
1869                )));
1870            }
1871        };
1872
1873        // Parse optional parameters
1874        let field_headers = if args.len() >= 4 {
1875            match args[3].value()?.into_literal() {
1876                LiteralValue::Int(i) => i as i32,
1877                LiteralValue::Number(n) => n as i32,
1878                _ => 1,
1879            }
1880        } else {
1881            1
1882        };
1883
1884        let total_depth = if args.len() >= 5 {
1885            match args[4].value()?.into_literal() {
1886                LiteralValue::Int(i) => i as i32,
1887                LiteralValue::Number(n) => n as i32,
1888                _ => 0,
1889            }
1890        } else {
1891            0
1892        };
1893
1894        let sort_order = if args.len() >= 6 {
1895            match args[5].value()?.into_literal() {
1896                LiteralValue::Int(i) => i as i32,
1897                LiteralValue::Number(n) => n as i32,
1898                _ => 0,
1899            }
1900        } else {
1901            0
1902        };
1903
1904        let (rf_rows, rf_cols) = row_fields_view.dims();
1905        let (val_rows, val_cols) = values_view.dims();
1906
1907        // Determine if we have headers
1908        let has_headers = field_headers == 1 || field_headers == 3;
1909        let data_start_row = if has_headers { 1 } else { 0 };
1910
1911        // Validate that row counts match (accounting for headers)
1912        if rf_rows != val_rows {
1913            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1914                ExcelError::new(ExcelErrorKind::Value)
1915                    .with_message("Row fields and values must have same number of rows"),
1916            )));
1917        }
1918
1919        if rf_rows <= data_start_row {
1920            // No data rows
1921            return Ok(crate::traits::CalcValue::Range(
1922                crate::engine::range_view::RangeView::from_owned_rows(vec![], _ctx.date_system()),
1923            ));
1924        }
1925
1926        // Build groups: key -> list of values for each value column
1927        // For multi-column row_fields, concatenate keys
1928        let mut groups: std::collections::HashMap<String, Vec<Vec<f64>>> = HashMap::new();
1929        let mut group_order: Vec<String> = Vec::new();
1930
1931        for r in data_start_row..rf_rows {
1932            // Build composite key from all row_field columns
1933            let mut key_parts: Vec<String> = Vec::with_capacity(rf_cols);
1934            for c in 0..rf_cols {
1935                key_parts.push(literal_to_group_key(&row_fields_view.get_cell(r, c)));
1936            }
1937            let key = key_parts.join("\x00"); // Use null separator for composite keys
1938
1939            // Get values for this row
1940            let mut row_values: Vec<Option<f64>> = Vec::with_capacity(val_cols);
1941            for c in 0..val_cols {
1942                row_values.push(literal_to_num_opt(&values_view.get_cell(r, c)));
1943            }
1944
1945            // Add to groups
1946            if !groups.contains_key(&key) {
1947                group_order.push(key.clone());
1948                groups.insert(key.clone(), vec![Vec::new(); val_cols]);
1949            }
1950
1951            let group_vals = groups.get_mut(&key).unwrap();
1952            for (c, val) in row_values.iter().enumerate() {
1953                if let Some(v) = val {
1954                    group_vals[c].push(*v);
1955                }
1956            }
1957        }
1958
1959        // Sort groups if requested
1960        if sort_order != 0 {
1961            group_order.sort_by(|a, b| if sort_order > 0 { a.cmp(b) } else { b.cmp(a) });
1962        }
1963
1964        // Build output
1965        let mut output: Vec<Vec<LiteralValue>> = Vec::new();
1966
1967        // Add header row if requested
1968        let generate_headers = field_headers == 2 || field_headers == 3;
1969        if generate_headers {
1970            let mut header_row: Vec<LiteralValue> = Vec::new();
1971            // Row field headers
1972            for c in 0..rf_cols {
1973                if has_headers {
1974                    header_row.push(row_fields_view.get_cell(0, c));
1975                } else {
1976                    header_row.push(LiteralValue::Text(format!("Field{}", c + 1)));
1977                }
1978            }
1979            // Value headers
1980            for c in 0..val_cols {
1981                if has_headers {
1982                    header_row.push(values_view.get_cell(0, c));
1983                } else {
1984                    header_row.push(LiteralValue::Text(format!("Value{}", c + 1)));
1985                }
1986            }
1987            output.push(header_row);
1988        }
1989
1990        // Add grouped data rows
1991        for key in &group_order {
1992            let mut row: Vec<LiteralValue> = Vec::new();
1993
1994            // Add row field values (split composite key)
1995            let key_parts: Vec<&str> = key.split('\x00').collect();
1996            for part in &key_parts {
1997                row.push(LiteralValue::Text(part.to_string()));
1998            }
1999
2000            // Add aggregated values
2001            let group_vals = groups.get(key).unwrap();
2002            for col_vals in group_vals {
2003                let result = aggregation.apply(col_vals);
2004                if result.is_nan() {
2005                    row.push(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na)));
2006                } else if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
2007                    row.push(LiteralValue::Int(result as i64));
2008                } else {
2009                    row.push(LiteralValue::Number(result));
2010                }
2011            }
2012            output.push(row);
2013        }
2014
2015        // Add grand total if requested
2016        if total_depth >= 1 {
2017            let mut total_row: Vec<LiteralValue> = Vec::new();
2018            // Empty cells for row fields (except first which says "Grand Total")
2019            total_row.push(LiteralValue::Text("Grand Total".to_string()));
2020            for _ in 1..rf_cols {
2021                total_row.push(LiteralValue::Empty);
2022            }
2023
2024            // Aggregate all values across all groups
2025            for c in 0..val_cols {
2026                let mut all_vals: Vec<f64> = Vec::new();
2027                for group_vals in groups.values() {
2028                    all_vals.extend(&group_vals[c]);
2029                }
2030                let result = aggregation.apply(&all_vals);
2031                if result.is_nan() {
2032                    total_row.push(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na)));
2033                } else if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
2034                    total_row.push(LiteralValue::Int(result as i64));
2035                } else {
2036                    total_row.push(LiteralValue::Number(result));
2037                }
2038            }
2039            output.push(total_row);
2040        }
2041
2042        if output.is_empty() {
2043            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2044                ExcelError::new(ExcelErrorKind::Calc),
2045            )));
2046        }
2047
2048        Ok(collapse_if_scalar(output, _ctx.date_system()))
2049    }
2050}
2051
2052/* ───────────────────────── PIVOTBY() ───────────────────────── */
2053
2054#[derive(Debug)]
2055pub struct PivotByFn;
2056
2057/// Builds a pivot-style summary matrix from row fields, column fields, and values.
2058///
2059/// `PIVOTBY` aggregates intersections of row keys and column keys into a dynamic result grid.
2060///
2061/// # Remarks
2062/// - `row_fields`, `col_fields`, and `values` must have matching row counts.
2063/// - Aggregation accepts text names (for example `"SUM"`) or numeric codes.
2064/// - Optional controls include header handling, row/column totals, and sort order.
2065/// - Current implementation uses the first column of `col_fields` and `values` for aggregation.
2066/// - Invalid setup returns `#VALUE!`.
2067///
2068/// # Examples
2069/// ```yaml,sandbox
2070/// title: "Pivot sales by region and quarter"
2071/// grid:
2072///   A1: "Region"
2073///   A2: "East"
2074///   A3: "East"
2075///   A4: "West"
2076///   B1: "Quarter"
2077///   B2: "Q1"
2078///   B3: "Q2"
2079///   B4: "Q1"
2080///   C1: "Sales"
2081///   C2: 100
2082///   C3: 150
2083///   C4: 120
2084/// formula: '=PIVOTBY(A1:A4,B1:B4,C1:C4,"SUM",3,0,1,0,1)'
2085/// expected: "Pivot table with regions as rows and quarters as columns"
2086/// ```
2087///
2088/// ```yaml,sandbox
2089/// title: "Pivot with totals enabled"
2090/// grid:
2091///   A1: "Dept"
2092///   A2: "Ops"
2093///   A3: "Ops"
2094///   A4: "Sales"
2095///   B1: "Month"
2096///   B2: "Jan"
2097///   B3: "Feb"
2098///   B4: "Jan"
2099///   C1: "Cost"
2100///   C2: 40
2101///   C3: 35
2102///   C4: 55
2103/// formula: '=PIVOTBY(A1:A4,B1:B4,C1:C4,"SUM",3,1,1,1,1)'
2104/// expected: "Pivot table including row and column totals"
2105/// ```
2106///
2107/// ```yaml,docs
2108/// related:
2109///   - GROUPBY
2110///   - SORTBY
2111///   - UNIQUE
2112/// faq:
2113///   - q: "What rows must align in PIVOTBY inputs?"
2114///     a: "row_fields, col_fields, and values must share the same row count; otherwise PIVOTBY returns #VALUE!."
2115///   - q: "What value range is currently aggregated?"
2116///     a: "Current implementation aggregates using the first value column (and first col_fields column for keys), so extra columns are not yet summarized independently."
2117/// ```
2118/// [formualizer-docgen:schema:start]
2119/// Name: PIVOTBY
2120/// Type: PivotByFn
2121/// Min args: 4
2122/// Max args: variadic
2123/// Variadic: true
2124/// Signature: PIVOTBY(arg1: range@range, arg2: range@range, arg3: range@range, arg4: any@scalar, arg5?: number@scalar, arg6?: number@scalar, arg7?: number@scalar, arg8?: number@scalar, arg9?...: number@scalar)
2125/// Arg schema: arg1{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg3{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg4{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg5{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg6{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg7{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg8{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg9{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}
2126/// Caps: PURE
2127/// [formualizer-docgen:schema:end]
2128impl Function for PivotByFn {
2129    func_caps!(PURE);
2130    fn name(&self) -> &'static str {
2131        "PIVOTBY"
2132    }
2133    fn min_args(&self) -> usize {
2134        4
2135    }
2136    fn variadic(&self) -> bool {
2137        true
2138    }
2139    fn arg_schema(&self) -> &'static [ArgSchema] {
2140        use once_cell::sync::Lazy;
2141        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
2142            vec![
2143                // row_fields - range to group rows by
2144                ArgSchema {
2145                    kinds: smallvec::smallvec![ArgKind::Range],
2146                    required: true,
2147                    by_ref: true,
2148                    shape: ShapeKind::Range,
2149                    coercion: CoercionPolicy::None,
2150                    max: None,
2151                    repeating: None,
2152                    default: None,
2153                },
2154                // col_fields - range to group columns by
2155                ArgSchema {
2156                    kinds: smallvec::smallvec![ArgKind::Range],
2157                    required: true,
2158                    by_ref: true,
2159                    shape: ShapeKind::Range,
2160                    coercion: CoercionPolicy::None,
2161                    max: None,
2162                    repeating: None,
2163                    default: None,
2164                },
2165                // values - range of values to aggregate
2166                ArgSchema {
2167                    kinds: smallvec::smallvec![ArgKind::Range],
2168                    required: true,
2169                    by_ref: true,
2170                    shape: ShapeKind::Range,
2171                    coercion: CoercionPolicy::None,
2172                    max: None,
2173                    repeating: None,
2174                    default: None,
2175                },
2176                // function - aggregation function
2177                ArgSchema {
2178                    kinds: smallvec::smallvec![ArgKind::Any],
2179                    required: true,
2180                    by_ref: false,
2181                    shape: ShapeKind::Scalar,
2182                    coercion: CoercionPolicy::None,
2183                    max: None,
2184                    repeating: None,
2185                    default: None,
2186                },
2187                // field_headers (optional)
2188                ArgSchema {
2189                    kinds: smallvec::smallvec![ArgKind::Number],
2190                    required: false,
2191                    by_ref: false,
2192                    shape: ShapeKind::Scalar,
2193                    coercion: CoercionPolicy::NumberLenientText,
2194                    max: None,
2195                    repeating: None,
2196                    default: Some(LiteralValue::Int(1)),
2197                },
2198                // row_total_depth (optional)
2199                ArgSchema {
2200                    kinds: smallvec::smallvec![ArgKind::Number],
2201                    required: false,
2202                    by_ref: false,
2203                    shape: ShapeKind::Scalar,
2204                    coercion: CoercionPolicy::NumberLenientText,
2205                    max: None,
2206                    repeating: None,
2207                    default: Some(LiteralValue::Int(0)),
2208                },
2209                // row_sort_order (optional)
2210                ArgSchema {
2211                    kinds: smallvec::smallvec![ArgKind::Number],
2212                    required: false,
2213                    by_ref: false,
2214                    shape: ShapeKind::Scalar,
2215                    coercion: CoercionPolicy::NumberLenientText,
2216                    max: None,
2217                    repeating: None,
2218                    default: Some(LiteralValue::Int(0)),
2219                },
2220                // col_total_depth (optional)
2221                ArgSchema {
2222                    kinds: smallvec::smallvec![ArgKind::Number],
2223                    required: false,
2224                    by_ref: false,
2225                    shape: ShapeKind::Scalar,
2226                    coercion: CoercionPolicy::NumberLenientText,
2227                    max: None,
2228                    repeating: None,
2229                    default: Some(LiteralValue::Int(0)),
2230                },
2231                // col_sort_order (optional)
2232                ArgSchema {
2233                    kinds: smallvec::smallvec![ArgKind::Number],
2234                    required: false,
2235                    by_ref: false,
2236                    shape: ShapeKind::Scalar,
2237                    coercion: CoercionPolicy::NumberLenientText,
2238                    max: None,
2239                    repeating: None,
2240                    default: Some(LiteralValue::Int(0)),
2241                },
2242            ]
2243        });
2244        &SCHEMA
2245    }
2246
2247    fn eval<'a, 'b, 'c>(
2248        &self,
2249        args: &'c [ArgumentHandle<'a, 'b>],
2250        _ctx: &dyn FunctionContext<'b>,
2251    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2252        if args.len() < 4 {
2253            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2254                ExcelError::new(ExcelErrorKind::Value),
2255            )));
2256        }
2257
2258        // Get ranges
2259        let row_fields_view = match args[0].range_view() {
2260            Ok(v) => v,
2261            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2262        };
2263        let col_fields_view = match args[1].range_view() {
2264            Ok(v) => v,
2265            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2266        };
2267        let values_view = match args[2].range_view() {
2268            Ok(v) => v,
2269            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2270        };
2271
2272        // Parse aggregation function
2273        let agg_val = args[3].value()?.into_literal();
2274        let aggregation = match GroupAggregation::from_literal(&agg_val) {
2275            Some(a) => a,
2276            None => {
2277                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2278                    ExcelError::new(ExcelErrorKind::Value)
2279                        .with_message("Invalid aggregation function"),
2280                )));
2281            }
2282        };
2283
2284        // Parse optional parameters
2285        let field_headers = if args.len() >= 5 {
2286            match args[4].value()?.into_literal() {
2287                LiteralValue::Int(i) => i as i32,
2288                LiteralValue::Number(n) => n as i32,
2289                _ => 1,
2290            }
2291        } else {
2292            1
2293        };
2294
2295        let row_total_depth = if args.len() >= 6 {
2296            match args[5].value()?.into_literal() {
2297                LiteralValue::Int(i) => i as i32,
2298                LiteralValue::Number(n) => n as i32,
2299                _ => 0,
2300            }
2301        } else {
2302            0
2303        };
2304
2305        let row_sort_order = if args.len() >= 7 {
2306            match args[6].value()?.into_literal() {
2307                LiteralValue::Int(i) => i as i32,
2308                LiteralValue::Number(n) => n as i32,
2309                _ => 0,
2310            }
2311        } else {
2312            0
2313        };
2314
2315        let col_total_depth = if args.len() >= 8 {
2316            match args[7].value()?.into_literal() {
2317                LiteralValue::Int(i) => i as i32,
2318                LiteralValue::Number(n) => n as i32,
2319                _ => 0,
2320            }
2321        } else {
2322            0
2323        };
2324
2325        let col_sort_order = if args.len() >= 9 {
2326            match args[8].value()?.into_literal() {
2327                LiteralValue::Int(i) => i as i32,
2328                LiteralValue::Number(n) => n as i32,
2329                _ => 0,
2330            }
2331        } else {
2332            0
2333        };
2334
2335        let (rf_rows, rf_cols) = row_fields_view.dims();
2336        let (cf_rows, _cf_cols) = col_fields_view.dims();
2337        let (val_rows, _val_cols) = values_view.dims();
2338
2339        // Validate dimensions
2340        if rf_rows != cf_rows || rf_rows != val_rows {
2341            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2342                ExcelError::new(ExcelErrorKind::Value)
2343                    .with_message("All ranges must have same number of rows"),
2344            )));
2345        }
2346
2347        let has_headers = field_headers == 1 || field_headers == 3;
2348        let data_start_row = if has_headers { 1 } else { 0 };
2349
2350        if rf_rows <= data_start_row {
2351            return Ok(crate::traits::CalcValue::Range(
2352                crate::engine::range_view::RangeView::from_owned_rows(vec![], _ctx.date_system()),
2353            ));
2354        }
2355
2356        // Collect unique row and column keys
2357        let mut row_keys: Vec<String> = Vec::new();
2358        let mut col_keys: Vec<String> = Vec::new();
2359        let mut row_key_set: std::collections::HashSet<String> = std::collections::HashSet::new();
2360        let mut col_key_set: std::collections::HashSet<String> = std::collections::HashSet::new();
2361
2362        // Build pivot data: (row_key, col_key) -> values
2363        let mut pivot_data: HashMap<(String, String), Vec<f64>> = HashMap::new();
2364
2365        for r in data_start_row..rf_rows {
2366            // Build row key
2367            let mut row_key_parts: Vec<String> = Vec::with_capacity(rf_cols);
2368            for c in 0..rf_cols {
2369                row_key_parts.push(literal_to_group_key(&row_fields_view.get_cell(r, c)));
2370            }
2371            let row_key = row_key_parts.join("\x00");
2372
2373            // Build col key (use first column of col_fields for simplicity)
2374            // TODO(phase6): PIVOTBY only uses first col of col_fields/values.
2375            let col_key = literal_to_group_key(&col_fields_view.get_cell(r, 0));
2376
2377            // Get value (use first column of values)
2378            let val = literal_to_num_opt(&values_view.get_cell(r, 0));
2379
2380            // Track unique keys in order
2381            if !row_key_set.contains(&row_key) {
2382                row_key_set.insert(row_key.clone());
2383                row_keys.push(row_key.clone());
2384            }
2385            if !col_key_set.contains(&col_key) {
2386                col_key_set.insert(col_key.clone());
2387                col_keys.push(col_key.clone());
2388            }
2389
2390            // Add to pivot data
2391            let entry = pivot_data.entry((row_key, col_key)).or_default();
2392            if let Some(v) = val {
2393                entry.push(v);
2394            }
2395        }
2396
2397        // Sort keys if requested
2398        if row_sort_order != 0 {
2399            row_keys.sort_by(|a, b| {
2400                if row_sort_order > 0 {
2401                    a.cmp(b)
2402                } else {
2403                    b.cmp(a)
2404                }
2405            });
2406        }
2407        if col_sort_order != 0 {
2408            col_keys.sort_by(|a, b| {
2409                if col_sort_order > 0 {
2410                    a.cmp(b)
2411                } else {
2412                    b.cmp(a)
2413                }
2414            });
2415        }
2416
2417        // Build output grid
2418        let mut output: Vec<Vec<LiteralValue>> = Vec::new();
2419
2420        // Header row: empty cells for row fields + column keys
2421        let generate_headers = field_headers == 2 || field_headers == 3;
2422        if generate_headers || has_headers {
2423            let mut header_row: Vec<LiteralValue> = Vec::new();
2424            // Empty cells for row field columns
2425            for _ in 0..rf_cols {
2426                header_row.push(LiteralValue::Empty);
2427            }
2428            // Column headers
2429            for col_key in &col_keys {
2430                // Split composite key and use the visible parts
2431                let parts: Vec<&str> = col_key.split('\x00').collect();
2432                header_row.push(LiteralValue::Text(parts.join(" ")));
2433            }
2434            // Total column header
2435            if col_total_depth >= 1 {
2436                header_row.push(LiteralValue::Text("Total".to_string()));
2437            }
2438            output.push(header_row);
2439        }
2440
2441        // Data rows
2442        for row_key in &row_keys {
2443            let mut row: Vec<LiteralValue> = Vec::new();
2444
2445            // Row field values
2446            let row_parts: Vec<&str> = row_key.split('\x00').collect();
2447            for part in &row_parts {
2448                row.push(LiteralValue::Text(part.to_string()));
2449            }
2450
2451            // Values for each column
2452            let mut row_total_vals: Vec<f64> = Vec::new();
2453            for col_key in &col_keys {
2454                let key = (row_key.clone(), col_key.clone());
2455                let vals = pivot_data.get(&key).map(|v| v.as_slice()).unwrap_or(&[]);
2456                let result = aggregation.apply(vals);
2457
2458                // Collect for row total
2459                row_total_vals.extend(vals);
2460
2461                if result.is_nan() || vals.is_empty() {
2462                    row.push(LiteralValue::Empty);
2463                } else if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
2464                    row.push(LiteralValue::Int(result as i64));
2465                } else {
2466                    row.push(LiteralValue::Number(result));
2467                }
2468            }
2469
2470            // Row total
2471            if col_total_depth >= 1 {
2472                let result = aggregation.apply(&row_total_vals);
2473                if result.is_nan() {
2474                    row.push(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na)));
2475                } else if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
2476                    row.push(LiteralValue::Int(result as i64));
2477                } else {
2478                    row.push(LiteralValue::Number(result));
2479                }
2480            }
2481
2482            output.push(row);
2483        }
2484
2485        // Grand total row
2486        if row_total_depth >= 1 {
2487            let mut total_row: Vec<LiteralValue> = Vec::new();
2488            total_row.push(LiteralValue::Text("Total".to_string()));
2489            for _ in 1..rf_cols {
2490                total_row.push(LiteralValue::Empty);
2491            }
2492
2493            let mut grand_total_vals: Vec<f64> = Vec::new();
2494            for col_key in &col_keys {
2495                let mut col_vals: Vec<f64> = Vec::new();
2496                for row_key in &row_keys {
2497                    let key = (row_key.clone(), col_key.clone());
2498                    if let Some(vals) = pivot_data.get(&key) {
2499                        col_vals.extend(vals);
2500                    }
2501                }
2502                grand_total_vals.extend(&col_vals);
2503                let result = aggregation.apply(&col_vals);
2504                if result.is_nan() {
2505                    total_row.push(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na)));
2506                } else if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
2507                    total_row.push(LiteralValue::Int(result as i64));
2508                } else {
2509                    total_row.push(LiteralValue::Number(result));
2510                }
2511            }
2512
2513            // Grand total of grand totals
2514            if col_total_depth >= 1 {
2515                let result = aggregation.apply(&grand_total_vals);
2516                if result.is_nan() {
2517                    total_row.push(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na)));
2518                } else if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
2519                    total_row.push(LiteralValue::Int(result as i64));
2520                } else {
2521                    total_row.push(LiteralValue::Number(result));
2522                }
2523            }
2524
2525            output.push(total_row);
2526        }
2527
2528        if output.is_empty() {
2529            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2530                ExcelError::new(ExcelErrorKind::Calc),
2531            )));
2532        }
2533
2534        Ok(collapse_if_scalar(output, _ctx.date_system()))
2535    }
2536}
2537
2538/* ───────────────────────── FILTER() ───────────────────────── */
2539
2540#[derive(Debug)]
2541pub struct FilterFn;
2542/// Filters rows from an array using a Boolean include mask.
2543///
2544/// `FILTER` returns only rows where the include condition evaluates to true.
2545///
2546/// # Remarks
2547/// - `include` must have the same row count as `array`, or a single row used as broadcast.
2548/// - A row is kept if any include cell for that row is truthy.
2549/// - If no rows match, `if_empty` is returned when supplied; otherwise `#CALC!`.
2550/// - Dimension mismatches return `#VALUE!`.
2551/// - Results spill as dynamic arrays.
2552///
2553/// # Examples
2554/// ```yaml,sandbox
2555/// title: "Filter active records"
2556/// grid:
2557///   A1: "Ana"
2558///   B1: true
2559///   A2: "Bo"
2560///   B2: false
2561///   A3: "Cy"
2562///   B3: true
2563/// formula: '=FILTER(A1:A3,B1:B3)'
2564/// expected: [["Ana"],["Cy"]]
2565/// ```
2566///
2567/// ```yaml,sandbox
2568/// title: "Return fallback when no rows match"
2569/// grid:
2570///   A1: 10
2571///   A2: 20
2572///   B1: false
2573///   B2: false
2574/// formula: '=FILTER(A1:A2,B1:B2,"No matches")'
2575/// expected: "No matches"
2576/// ```
2577///
2578/// ```yaml,docs
2579/// related:
2580///   - XLOOKUP
2581///   - UNIQUE
2582///   - SORT
2583/// faq:
2584///   - q: "What happens when include has no TRUE rows?"
2585///     a: "FILTER returns if_empty when provided; otherwise it returns #CALC! to signal an empty result set."
2586///   - q: "How strict is include shape matching?"
2587///     a: "include must match array row count or be a single broadcast row; incompatible dimensions return #VALUE!."
2588/// ```
2589/// [formualizer-docgen:schema:start]
2590/// Name: FILTER
2591/// Type: FilterFn
2592/// Min args: 2
2593/// Max args: variadic
2594/// Variadic: true
2595/// Signature: FILTER(arg1: range@range, arg2: range@range, arg3?...: any@scalar)
2596/// Arg schema: arg1{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg3{kinds=any,required=false,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
2597/// Caps: PURE
2598/// [formualizer-docgen:schema:end]
2599impl Function for FilterFn {
2600    func_caps!(PURE);
2601    fn name(&self) -> &'static str {
2602        "FILTER"
2603    }
2604    fn min_args(&self) -> usize {
2605        2
2606    }
2607    fn variadic(&self) -> bool {
2608        true
2609    }
2610    fn arg_schema(&self) -> &'static [ArgSchema] {
2611        use once_cell::sync::Lazy;
2612        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
2613            vec![
2614                // array
2615                ArgSchema {
2616                    kinds: smallvec::smallvec![ArgKind::Range],
2617                    required: true,
2618                    by_ref: true,
2619                    shape: ShapeKind::Range,
2620                    coercion: CoercionPolicy::None,
2621                    max: None,
2622                    repeating: None,
2623                    default: None,
2624                },
2625                // include
2626                ArgSchema {
2627                    kinds: smallvec::smallvec![ArgKind::Range],
2628                    required: true,
2629                    by_ref: true,
2630                    shape: ShapeKind::Range,
2631                    coercion: CoercionPolicy::None,
2632                    max: None,
2633                    repeating: None,
2634                    default: None,
2635                },
2636                // if_empty optional scalar
2637                ArgSchema {
2638                    kinds: smallvec::smallvec![ArgKind::Any],
2639                    required: false,
2640                    by_ref: false,
2641                    shape: ShapeKind::Scalar,
2642                    coercion: CoercionPolicy::None,
2643                    max: None,
2644                    repeating: None,
2645                    default: None,
2646                },
2647            ]
2648        });
2649        &SCHEMA
2650    }
2651    fn eval<'a, 'b, 'c>(
2652        &self,
2653        args: &'c [ArgumentHandle<'a, 'b>],
2654        _ctx: &dyn FunctionContext<'b>,
2655    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2656        if args.len() < 2 {
2657            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2658                ExcelError::new(ExcelErrorKind::Value),
2659            )));
2660        }
2661        let array_view = args[0].range_view()?;
2662        let include_view = args[1].range_view()?;
2663
2664        let (array_rows, array_cols) = array_view.dims();
2665        if array_rows == 0 || array_cols == 0 {
2666            return Ok(crate::traits::CalcValue::Range(
2667                crate::engine::range_view::RangeView::from_owned_rows(vec![], _ctx.date_system()),
2668            ));
2669        }
2670
2671        let (include_rows, include_cols) = include_view.dims();
2672        if include_rows != array_rows && include_rows != 1 {
2673            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2674                ExcelError::new(ExcelErrorKind::Value),
2675            )));
2676        }
2677
2678        let mut result: Vec<Vec<LiteralValue>> = Vec::new();
2679        for r in 0..array_rows {
2680            let include_r = if include_rows == array_rows { r } else { 0 };
2681            let mut include = false;
2682            for c in 0..include_cols {
2683                if include_view.get_cell(include_r, c).is_truthy() {
2684                    include = true;
2685                    break;
2686                }
2687            }
2688
2689            if include {
2690                let mut row_out: Vec<LiteralValue> = Vec::with_capacity(array_cols);
2691                for c in 0..array_cols {
2692                    row_out.push(array_view.get_cell(r, c));
2693                }
2694                result.push(row_out);
2695            }
2696        }
2697
2698        if result.is_empty() {
2699            if args.len() >= 3 {
2700                return args[2].value();
2701            }
2702            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
2703                ExcelError::new(ExcelErrorKind::Calc),
2704            )));
2705        }
2706
2707        Ok(crate::traits::CalcValue::Range(
2708            crate::engine::range_view::RangeView::from_owned_rows(result, _ctx.date_system()),
2709        ))
2710    }
2711}
2712
2713/* ───────────────────────── UNIQUE() ───────────────────────── */
2714
2715#[derive(Debug)]
2716pub struct UniqueFn;
2717/// Returns distinct rows or columns from a range.
2718///
2719/// `UNIQUE` preserves first-occurrence order and can optionally return only values that appear once.
2720///
2721/// # Remarks
2722/// - Defaults: `by_col=FALSE`, `exactly_once=FALSE`.
2723/// - With `by_col=FALSE`, uniqueness is evaluated by full rows.
2724/// - With `by_col=TRUE`, uniqueness is evaluated by full columns.
2725/// - With `exactly_once=TRUE`, only entries with frequency 1 are returned.
2726/// - Result spills as an array.
2727///
2728/// # Examples
2729/// ```yaml,sandbox
2730/// title: "Unique values by row"
2731/// grid:
2732///   A1: "A"
2733///   A2: "A"
2734///   A3: "B"
2735///   A4: "C"
2736/// formula: '=UNIQUE(A1:A4)'
2737/// expected: [["A"],["B"],["C"]]
2738/// ```
2739///
2740/// ```yaml,sandbox
2741/// title: "Only values that appear once"
2742/// grid:
2743///   A1: 1
2744///   A2: 1
2745///   A3: 2
2746///   A4: 3
2747/// formula: '=UNIQUE(A1:A4,FALSE,TRUE)'
2748/// expected: [[2],[3]]
2749/// ```
2750///
2751/// ```yaml,docs
2752/// related:
2753///   - FILTER
2754///   - SORT
2755///   - SORTBY
2756/// faq:
2757///   - q: "What does exactly_once=TRUE change?"
2758///     a: "Instead of returning first occurrences, UNIQUE returns only rows or columns whose full key appears exactly one time."
2759///   - q: "How does by_col affect uniqueness checks?"
2760///     a: "by_col=FALSE compares entire rows, while by_col=TRUE compares entire columns and spills unique columns."
2761/// ```
2762/// [formualizer-docgen:schema:start]
2763/// Name: UNIQUE
2764/// Type: UniqueFn
2765/// Min args: 1
2766/// Max args: variadic
2767/// Variadic: true
2768/// Signature: UNIQUE(arg1: range@range, arg2?: logical@scalar, arg3?...: logical@scalar)
2769/// Arg schema: arg1{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=logical,required=false,shape=scalar,by_ref=false,coercion=Logical,max=None,repeating=None,default=true}; arg3{kinds=logical,required=false,shape=scalar,by_ref=false,coercion=Logical,max=None,repeating=None,default=true}
2770/// Caps: PURE
2771/// [formualizer-docgen:schema:end]
2772impl Function for UniqueFn {
2773    func_caps!(PURE);
2774    fn name(&self) -> &'static str {
2775        "UNIQUE"
2776    }
2777    fn min_args(&self) -> usize {
2778        1
2779    }
2780    fn variadic(&self) -> bool {
2781        true
2782    }
2783    fn arg_schema(&self) -> &'static [ArgSchema] {
2784        use once_cell::sync::Lazy;
2785        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
2786            vec![
2787                ArgSchema {
2788                    kinds: smallvec::smallvec![ArgKind::Range],
2789                    required: true,
2790                    by_ref: true,
2791                    shape: ShapeKind::Range,
2792                    coercion: CoercionPolicy::None,
2793                    max: None,
2794                    repeating: None,
2795                    default: None,
2796                },
2797                ArgSchema {
2798                    kinds: smallvec::smallvec![ArgKind::Logical],
2799                    required: false,
2800                    by_ref: false,
2801                    shape: ShapeKind::Scalar,
2802                    coercion: CoercionPolicy::Logical,
2803                    max: None,
2804                    repeating: None,
2805                    default: Some(LiteralValue::Boolean(false)),
2806                },
2807                ArgSchema {
2808                    kinds: smallvec::smallvec![ArgKind::Logical],
2809                    required: false,
2810                    by_ref: false,
2811                    shape: ShapeKind::Scalar,
2812                    coercion: CoercionPolicy::Logical,
2813                    max: None,
2814                    repeating: None,
2815                    default: Some(LiteralValue::Boolean(false)),
2816                },
2817            ]
2818        });
2819        &SCHEMA
2820    }
2821    fn eval<'a, 'b, 'c>(
2822        &self,
2823        args: &'c [ArgumentHandle<'a, 'b>],
2824        _ctx: &dyn FunctionContext<'b>,
2825    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2826        let view = match args[0].range_view() {
2827            Ok(v) => v,
2828            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
2829        };
2830        let (rows, cols) = view.dims();
2831        if rows == 0 || cols == 0 {
2832            return Ok(crate::traits::CalcValue::Range(
2833                crate::engine::range_view::RangeView::from_owned_rows(vec![], _ctx.date_system()),
2834            ));
2835        }
2836
2837        let by_col = if args.len() >= 2 {
2838            matches!(args[1].value()?.into_literal(), LiteralValue::Boolean(true))
2839        } else {
2840            false
2841        };
2842        let exactly_once = if args.len() >= 3 {
2843            matches!(args[2].value()?.into_literal(), LiteralValue::Boolean(true))
2844        } else {
2845            false
2846        };
2847
2848        if by_col {
2849            #[derive(Hash, Eq, PartialEq, Clone)]
2850            struct ColKey(Vec<LiteralValue>);
2851
2852            let mut order: Vec<ColKey> = Vec::new();
2853            let mut counts: HashMap<ColKey, usize> = HashMap::new();
2854
2855            for c in 0..cols {
2856                let mut col_vals: Vec<LiteralValue> = Vec::with_capacity(rows);
2857                for r in 0..rows {
2858                    col_vals.push(view.get_cell(r, c));
2859                }
2860                let key = ColKey(col_vals);
2861                if !counts.contains_key(&key) {
2862                    order.push(key.clone());
2863                }
2864                *counts.entry(key).or_insert(0) += 1;
2865            }
2866
2867            let mut out: Vec<Vec<LiteralValue>> = Vec::new();
2868            for k in order {
2869                if !exactly_once || counts.get(&k) == Some(&1) {
2870                    out.push(k.0);
2871                }
2872            }
2873            return Ok(collapse_if_scalar(out, _ctx.date_system()));
2874        }
2875
2876        #[derive(Hash, Eq, PartialEq, Clone)]
2877        struct RowKey(Vec<LiteralValue>);
2878
2879        let mut order: Vec<RowKey> = Vec::new();
2880        let mut counts: HashMap<RowKey, usize> = HashMap::new();
2881        for r in 0..rows {
2882            let mut row_vals: Vec<LiteralValue> = Vec::with_capacity(cols);
2883            for c in 0..cols {
2884                row_vals.push(view.get_cell(r, c));
2885            }
2886            let key = RowKey(row_vals);
2887            if !counts.contains_key(&key) {
2888                order.push(key.clone());
2889            }
2890            *counts.entry(key).or_insert(0) += 1;
2891        }
2892
2893        let mut out: Vec<Vec<LiteralValue>> = Vec::new();
2894        for k in order {
2895            if !exactly_once || counts.get(&k) == Some(&1) {
2896                out.push(k.0);
2897            }
2898        }
2899        Ok(collapse_if_scalar(out, _ctx.date_system()))
2900    }
2901}
2902
2903/* ───────────────────────── SEQUENCE() ───────────────────────── */
2904
2905#[derive(Debug)]
2906pub struct SequenceFn;
2907/// Generates a sequential numeric array with configurable size, start, and step.
2908///
2909/// `SEQUENCE` fills values row-by-row and returns a dynamic spill.
2910///
2911/// # Remarks
2912/// - Defaults: `columns=1`, `start=1`, `step=1`.
2913/// - `rows` and `columns` must be positive; otherwise returns `#VALUE!`.
2914/// - Values are emitted as integers when integral, otherwise as floating-point numbers.
2915/// - Result spills to the requested dimensions.
2916///
2917/// # Examples
2918/// ```yaml,sandbox
2919/// title: "Simple vertical sequence"
2920/// formula: '=SEQUENCE(5)'
2921/// expected: [[1],[2],[3],[4],[5]]
2922/// ```
2923///
2924/// ```yaml,sandbox
2925/// title: "2x3 sequence with custom start and step"
2926/// formula: '=SEQUENCE(2,3,10,5)'
2927/// expected: [[10,15,20],[25,30,35]]
2928/// ```
2929///
2930/// ```yaml,docs
2931/// related:
2932///   - RANDARRAY
2933///   - TAKE
2934///   - DROP
2935/// faq:
2936///   - q: "What input values are invalid for SEQUENCE?"
2937///     a: "rows and columns must be positive numbers; zero or negative sizes return #VALUE!."
2938///   - q: "Does SEQUENCE fill by rows or by columns first?"
2939///     a: "SEQUENCE fills row-by-row across columns, then continues on the next row using the same step increment."
2940/// ```
2941/// [formualizer-docgen:schema:start]
2942/// Name: SEQUENCE
2943/// Type: SequenceFn
2944/// Min args: 1
2945/// Max args: variadic
2946/// Variadic: true
2947/// Signature: SEQUENCE(arg1: number@scalar, arg2?: number@scalar, arg3?: number@scalar, arg4?...: number@scalar)
2948/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg3{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}; arg4{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=true}
2949/// Caps: PURE
2950/// [formualizer-docgen:schema:end]
2951impl Function for SequenceFn {
2952    func_caps!(PURE);
2953    fn name(&self) -> &'static str {
2954        "SEQUENCE"
2955    }
2956    fn min_args(&self) -> usize {
2957        1
2958    }
2959    fn variadic(&self) -> bool {
2960        true
2961    }
2962    fn arg_schema(&self) -> &'static [ArgSchema] {
2963        use once_cell::sync::Lazy;
2964        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
2965            vec![
2966                // rows
2967                ArgSchema {
2968                    kinds: smallvec::smallvec![ArgKind::Number],
2969                    required: true,
2970                    by_ref: false,
2971                    shape: ShapeKind::Scalar,
2972                    coercion: CoercionPolicy::NumberLenientText,
2973                    max: None,
2974                    repeating: None,
2975                    default: None,
2976                },
2977                // columns (default 1)
2978                ArgSchema {
2979                    kinds: smallvec::smallvec![ArgKind::Number],
2980                    required: false,
2981                    by_ref: false,
2982                    shape: ShapeKind::Scalar,
2983                    coercion: CoercionPolicy::NumberLenientText,
2984                    max: None,
2985                    repeating: None,
2986                    default: Some(LiteralValue::Int(1)),
2987                },
2988                // start (default 1)
2989                ArgSchema {
2990                    kinds: smallvec::smallvec![ArgKind::Number],
2991                    required: false,
2992                    by_ref: false,
2993                    shape: ShapeKind::Scalar,
2994                    coercion: CoercionPolicy::NumberLenientText,
2995                    max: None,
2996                    repeating: None,
2997                    default: Some(LiteralValue::Int(1)),
2998                },
2999                // step (default 1)
3000                ArgSchema {
3001                    kinds: smallvec::smallvec![ArgKind::Number],
3002                    required: false,
3003                    by_ref: false,
3004                    shape: ShapeKind::Scalar,
3005                    coercion: CoercionPolicy::NumberLenientText,
3006                    max: None,
3007                    repeating: None,
3008                    default: Some(LiteralValue::Int(1)),
3009                },
3010            ]
3011        });
3012        &SCHEMA
3013    }
3014    fn eval<'a, 'b, 'c>(
3015        &self,
3016        args: &'c [ArgumentHandle<'a, 'b>],
3017        _ctx: &dyn FunctionContext<'b>,
3018    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3019        // Extract numbers (allow float but coerce to i64 for dimensions)
3020        let num = |a: &ArgumentHandle| -> Result<f64, ExcelError> {
3021            Ok(match a.value()?.into_literal() {
3022                LiteralValue::Int(i) => i as f64,
3023                LiteralValue::Number(n) => n,
3024                _other => {
3025                    return Err(ExcelError::new(ExcelErrorKind::Value));
3026                }
3027            })
3028        };
3029        let rows_f = num(&args[0])?;
3030        let rows = rows_f as i64;
3031        let cols = if args.len() >= 2 {
3032            num(&args[1])? as i64
3033        } else {
3034            1
3035        };
3036        let start = if args.len() >= 3 { num(&args[2])? } else { 1.0 };
3037        let step = if args.len() >= 4 { num(&args[3])? } else { 1.0 };
3038        if rows <= 0 || cols <= 0 {
3039            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3040                ExcelError::new(ExcelErrorKind::Value),
3041            )));
3042        }
3043        let total = rows.saturating_mul(cols);
3044        // TODO(perf): guard extremely large allocations (#NUM!).
3045        let mut out: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows as usize);
3046        let mut current = start;
3047        for _r in 0..rows {
3048            let mut row_vec: Vec<LiteralValue> = Vec::with_capacity(cols as usize);
3049            for _c in 0..cols {
3050                // Use Int when value integral & within i64 range
3051                if (current.fract().abs() < 1e-12) && current.abs() < (i64::MAX as f64) {
3052                    row_vec.push(LiteralValue::Int(current as i64));
3053                } else {
3054                    row_vec.push(LiteralValue::Number(current));
3055                }
3056                current += step;
3057            }
3058            out.push(row_vec);
3059        }
3060
3061        Ok(collapse_if_scalar(out, _ctx.date_system()))
3062    }
3063}
3064
3065/* ───────────────────────── TRANSPOSE() ───────────────────────── */
3066
3067#[derive(Debug)]
3068pub struct TransposeFn;
3069/// Swaps rows and columns in an input range.
3070///
3071/// `TRANSPOSE` returns a spilled array whose shape is the inverse of the source shape.
3072///
3073/// # Remarks
3074/// - Input with shape `R x C` returns output shape `C x R`.
3075/// - Empty input returns an empty spill.
3076/// - Errors in source cells are preserved in transposed positions.
3077///
3078/// # Examples
3079/// ```yaml,sandbox
3080/// title: "Transpose a row into a column"
3081/// grid:
3082///   A1: 10
3083///   B1: 20
3084///   C1: 30
3085/// formula: '=TRANSPOSE(A1:C1)'
3086/// expected: [[10],[20],[30]]
3087/// ```
3088///
3089/// ```yaml,sandbox
3090/// title: "Transpose a 2x2 matrix"
3091/// grid:
3092///   A1: 1
3093///   B1: 2
3094///   A2: 3
3095///   B2: 4
3096/// formula: '=TRANSPOSE(A1:B2)'
3097/// expected: [[1,3],[2,4]]
3098/// ```
3099///
3100/// ```yaml,docs
3101/// related:
3102///   - TAKE
3103///   - DROP
3104///   - HSTACK
3105/// faq:
3106///   - q: "What happens to errors inside the source array?"
3107///     a: "TRANSPOSE preserves error values and only changes their position in the output matrix."
3108///   - q: "Does TRANSPOSE return a scalar for 1x1 inputs?"
3109///     a: "Yes. After transposition, a 1x1 result collapses to a scalar in this engine."
3110/// ```
3111/// [formualizer-docgen:schema:start]
3112/// Name: TRANSPOSE
3113/// Type: TransposeFn
3114/// Min args: 1
3115/// Max args: 1
3116/// Variadic: false
3117/// Signature: TRANSPOSE(arg1: range@range)
3118/// Arg schema: arg1{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}
3119/// Caps: PURE
3120/// [formualizer-docgen:schema:end]
3121impl Function for TransposeFn {
3122    func_caps!(PURE);
3123    fn name(&self) -> &'static str {
3124        "TRANSPOSE"
3125    }
3126    fn min_args(&self) -> usize {
3127        1
3128    }
3129    fn variadic(&self) -> bool {
3130        false
3131    }
3132    fn arg_schema(&self) -> &'static [ArgSchema] {
3133        use once_cell::sync::Lazy;
3134        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
3135            vec![ArgSchema {
3136                kinds: smallvec::smallvec![ArgKind::Range],
3137                required: true,
3138                by_ref: true,
3139                shape: ShapeKind::Range,
3140                coercion: CoercionPolicy::None,
3141                max: None,
3142                repeating: None,
3143                default: None,
3144            }]
3145        });
3146        &SCHEMA
3147    }
3148    fn eval<'a, 'b, 'c>(
3149        &self,
3150        args: &'c [ArgumentHandle<'a, 'b>],
3151        _ctx: &dyn FunctionContext<'b>,
3152    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3153        let view = match args[0].range_view() {
3154            Ok(v) => v,
3155            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3156        };
3157        let (rows, cols) = view.dims();
3158        if rows == 0 || cols == 0 {
3159            return Ok(crate::traits::CalcValue::Range(
3160                crate::engine::range_view::RangeView::from_owned_rows(vec![], _ctx.date_system()),
3161            ));
3162        }
3163
3164        let mut out: Vec<Vec<LiteralValue>> = vec![Vec::with_capacity(rows); cols];
3165        for (c, col) in out.iter_mut().enumerate().take(cols) {
3166            for r in 0..rows {
3167                col.push(view.get_cell(r, c));
3168            }
3169        }
3170        Ok(collapse_if_scalar(out, _ctx.date_system()))
3171    }
3172}
3173
3174/* ───────────────────────── TAKE() ───────────────────────── */
3175
3176#[derive(Debug)]
3177pub struct TakeFn;
3178/// Returns a subset from the start or end of rows and optional columns.
3179///
3180/// `TAKE` extracts leading or trailing portions of an array based on signed counts.
3181///
3182/// # Remarks
3183/// - Positive counts take from the start; negative counts take from the end.
3184/// - `rows` is required; `columns` is optional.
3185/// - Absolute counts larger than the source dimension return `#VALUE!`.
3186/// - Empty selections return an empty spill.
3187///
3188/// # Examples
3189/// ```yaml,sandbox
3190/// title: "Take top two rows"
3191/// grid:
3192///   A1: 10
3193///   A2: 20
3194///   A3: 30
3195/// formula: '=TAKE(A1:A3,2)'
3196/// expected: [[10],[20]]
3197/// ```
3198///
3199/// ```yaml,sandbox
3200/// title: "Take last row and last two columns"
3201/// grid:
3202///   A1: 1
3203///   B1: 2
3204///   C1: 3
3205///   A2: 4
3206///   B2: 5
3207///   C2: 6
3208/// formula: '=TAKE(A1:C2,-1,-2)'
3209/// expected: [[5,6]]
3210/// ```
3211///
3212/// ```yaml,docs
3213/// related:
3214///   - DROP
3215///   - CHOOSEROWS
3216///   - CHOOSECOLS
3217/// faq:
3218///   - q: "How are negative rows or columns interpreted?"
3219///     a: "Negative counts take from the end of the array, so TAKE(...,-1) returns the last row and TAKE(...,,-1) returns the last column."
3220///   - q: "When does TAKE return #VALUE!?"
3221///     a: "If the absolute requested row or column count exceeds source dimensions, TAKE returns #VALUE!."
3222/// ```
3223/// [formualizer-docgen:schema:start]
3224/// Name: TAKE
3225/// Type: TakeFn
3226/// Min args: 2
3227/// Max args: variadic
3228/// Variadic: true
3229/// Signature: TAKE(arg1: range@range, arg2: number@scalar, arg3?...: number@scalar)
3230/// Arg schema: arg1{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3231/// Caps: PURE
3232/// [formualizer-docgen:schema:end]
3233impl Function for TakeFn {
3234    func_caps!(PURE);
3235    fn name(&self) -> &'static str {
3236        "TAKE"
3237    }
3238    fn min_args(&self) -> usize {
3239        2
3240    }
3241    fn variadic(&self) -> bool {
3242        true
3243    }
3244    fn arg_schema(&self) -> &'static [ArgSchema] {
3245        use once_cell::sync::Lazy;
3246        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
3247            vec![
3248                ArgSchema {
3249                    kinds: smallvec::smallvec![ArgKind::Range],
3250                    required: true,
3251                    by_ref: true,
3252                    shape: ShapeKind::Range,
3253                    coercion: CoercionPolicy::None,
3254                    max: None,
3255                    repeating: None,
3256                    default: None,
3257                },
3258                ArgSchema {
3259                    kinds: smallvec::smallvec![ArgKind::Number],
3260                    required: true,
3261                    by_ref: false,
3262                    shape: ShapeKind::Scalar,
3263                    coercion: CoercionPolicy::NumberLenientText,
3264                    max: None,
3265                    repeating: None,
3266                    default: None,
3267                },
3268                ArgSchema {
3269                    kinds: smallvec::smallvec![ArgKind::Number],
3270                    required: false,
3271                    by_ref: false,
3272                    shape: ShapeKind::Scalar,
3273                    coercion: CoercionPolicy::NumberLenientText,
3274                    max: None,
3275                    repeating: None,
3276                    default: None,
3277                },
3278            ]
3279        });
3280        &SCHEMA
3281    }
3282    fn eval<'a, 'b, 'c>(
3283        &self,
3284        args: &'c [ArgumentHandle<'a, 'b>],
3285        _ctx: &dyn FunctionContext<'b>,
3286    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3287        let view = match args[0].range_view() {
3288            Ok(v) => v,
3289            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3290        };
3291        let (rows, cols) = view.dims();
3292        if rows == 0 || cols == 0 {
3293            return Ok(crate::traits::CalcValue::Range(
3294                crate::engine::range_view::RangeView::from_owned_rows(vec![], _ctx.date_system()),
3295            ));
3296        }
3297
3298        let height = rows as i64;
3299        let width = cols as i64;
3300
3301        let num = |a: &ArgumentHandle| -> Result<i64, ExcelError> {
3302            Ok(match a.value()?.into_literal() {
3303                LiteralValue::Int(i) => i,
3304                LiteralValue::Number(n) => n as i64,
3305                _ => 0,
3306            })
3307        };
3308        let take_rows = num(&args[1])?;
3309        let take_cols = if args.len() >= 3 {
3310            Some(num(&args[2])?)
3311        } else {
3312            None
3313        };
3314
3315        if take_rows.abs() > height {
3316            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3317                ExcelError::new(ExcelErrorKind::Value),
3318            )));
3319        }
3320
3321        let (row_start, row_end) = if take_rows >= 0 {
3322            (0usize, take_rows as usize)
3323        } else {
3324            ((height + take_rows) as usize, height as usize)
3325        };
3326
3327        let (col_start, col_end) = if let Some(tc) = take_cols {
3328            if tc.abs() > width {
3329                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
3330                    ExcelError::new(ExcelErrorKind::Value),
3331                )));
3332            }
3333            if tc >= 0 {
3334                (0usize, tc as usize)
3335            } else {
3336                ((width + tc) as usize, width as usize)
3337            }
3338        } else {
3339            (0usize, width as usize)
3340        };
3341
3342        if row_start >= row_end || col_start >= col_end {
3343            return Ok(crate::traits::CalcValue::Range(
3344                crate::engine::range_view::RangeView::from_owned_rows(vec![], _ctx.date_system()),
3345            ));
3346        }
3347
3348        let mut out: Vec<Vec<LiteralValue>> = Vec::with_capacity(row_end - row_start);
3349        for r in row_start..row_end {
3350            let mut row_out: Vec<LiteralValue> = Vec::with_capacity(col_end - col_start);
3351            for c in col_start..col_end {
3352                row_out.push(view.get_cell(r, c));
3353            }
3354            out.push(row_out);
3355        }
3356
3357        Ok(collapse_if_scalar(out, _ctx.date_system()))
3358    }
3359}
3360
3361/* ───────────────────────── DROP() ───────────────────────── */
3362
3363#[derive(Debug)]
3364pub struct DropFn;
3365/// Removes rows and optional columns from the start or end of an array.
3366///
3367/// `DROP` is the complement of `TAKE` and returns the remaining spilled subset.
3368///
3369/// # Remarks
3370/// - Positive counts drop from the start; negative counts drop from the end.
3371/// - `rows` is required; `columns` is optional.
3372/// - Dropping all rows or columns yields an empty spill.
3373/// - Invalid source references propagate errors.
3374///
3375/// # Examples
3376/// ```yaml,sandbox
3377/// title: "Drop header row"
3378/// grid:
3379///   A1: "Month"
3380///   A2: "Jan"
3381///   A3: "Feb"
3382/// formula: '=DROP(A1:A3,1)'
3383/// expected: [["Jan"],["Feb"]]
3384/// ```
3385///
3386/// ```yaml,sandbox
3387/// title: "Drop last column"
3388/// grid:
3389///   A1: 10
3390///   B1: 20
3391///   C1: 30
3392/// formula: '=DROP(A1:C1,0,-1)'
3393/// expected: [[10,20]]
3394/// ```
3395///
3396/// ```yaml,docs
3397/// related:
3398///   - TAKE
3399///   - CHOOSEROWS
3400///   - CHOOSECOLS
3401/// faq:
3402///   - q: "What does a negative drop count mean?"
3403///     a: "Negative counts drop from the end, so DROP(array,0,-1) removes the last column and keeps the leading columns."
3404///   - q: "What if DROP removes every row or column?"
3405///     a: "The result is an empty spill rather than an error when all rows or all columns are removed."
3406/// ```
3407/// [formualizer-docgen:schema:start]
3408/// Name: DROP
3409/// Type: DropFn
3410/// Min args: 2
3411/// Max args: variadic
3412/// Variadic: true
3413/// Signature: DROP(arg1: range@range, arg2: number@scalar, arg3?...: number@scalar)
3414/// Arg schema: arg1{kinds=range,required=true,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
3415/// Caps: PURE
3416/// [formualizer-docgen:schema:end]
3417impl Function for DropFn {
3418    func_caps!(PURE);
3419    fn name(&self) -> &'static str {
3420        "DROP"
3421    }
3422    fn min_args(&self) -> usize {
3423        2
3424    }
3425    fn variadic(&self) -> bool {
3426        true
3427    }
3428    fn arg_schema(&self) -> &'static [ArgSchema] {
3429        use once_cell::sync::Lazy;
3430        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
3431            vec![
3432                ArgSchema {
3433                    kinds: smallvec::smallvec![ArgKind::Range],
3434                    required: true,
3435                    by_ref: true,
3436                    shape: ShapeKind::Range,
3437                    coercion: CoercionPolicy::None,
3438                    max: None,
3439                    repeating: None,
3440                    default: None,
3441                },
3442                ArgSchema {
3443                    kinds: smallvec::smallvec![ArgKind::Number],
3444                    required: true,
3445                    by_ref: false,
3446                    shape: ShapeKind::Scalar,
3447                    coercion: CoercionPolicy::NumberLenientText,
3448                    max: None,
3449                    repeating: None,
3450                    default: None,
3451                },
3452                ArgSchema {
3453                    kinds: smallvec::smallvec![ArgKind::Number],
3454                    required: false,
3455                    by_ref: false,
3456                    shape: ShapeKind::Scalar,
3457                    coercion: CoercionPolicy::NumberLenientText,
3458                    max: None,
3459                    repeating: None,
3460                    default: None,
3461                },
3462            ]
3463        });
3464        &SCHEMA
3465    }
3466    fn eval<'a, 'b, 'c>(
3467        &self,
3468        args: &'c [ArgumentHandle<'a, 'b>],
3469        _ctx: &dyn FunctionContext<'b>,
3470    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
3471        let view = match args[0].range_view() {
3472            Ok(v) => v,
3473            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
3474        };
3475        let (rows, cols) = view.dims();
3476        if rows == 0 || cols == 0 {
3477            return Ok(crate::traits::CalcValue::Range(
3478                crate::engine::range_view::RangeView::from_owned_rows(vec![], _ctx.date_system()),
3479            ));
3480        }
3481
3482        let height = rows as i64;
3483        let width = cols as i64;
3484
3485        let num = |a: &ArgumentHandle| -> Result<i64, ExcelError> {
3486            Ok(match a.value()?.into_literal() {
3487                LiteralValue::Int(i) => i,
3488                LiteralValue::Number(n) => n as i64,
3489                _ => 0,
3490            })
3491        };
3492        let drop_rows = num(&args[1])?;
3493        let drop_cols = if args.len() >= 3 {
3494            Some(num(&args[2])?)
3495        } else {
3496            None
3497        };
3498
3499        let (row_start, row_end) = if drop_rows >= 0 {
3500            ((drop_rows as usize).min(height as usize), height as usize)
3501        } else {
3502            (0usize, (height + drop_rows).max(0) as usize)
3503        };
3504
3505        let (col_start, col_end) = if let Some(dc) = drop_cols {
3506            if dc >= 0 {
3507                ((dc as usize).min(width as usize), width as usize)
3508            } else {
3509                (0usize, (width + dc).max(0) as usize)
3510            }
3511        } else {
3512            (0usize, width as usize)
3513        };
3514
3515        if row_start >= row_end || col_start >= col_end {
3516            return Ok(crate::traits::CalcValue::Range(
3517                crate::engine::range_view::RangeView::from_owned_rows(vec![], _ctx.date_system()),
3518            ));
3519        }
3520
3521        let mut out: Vec<Vec<LiteralValue>> = Vec::with_capacity(row_end - row_start);
3522        for r in row_start..row_end {
3523            let mut row_out: Vec<LiteralValue> = Vec::with_capacity(col_end - col_start);
3524            for c in col_start..col_end {
3525                row_out.push(view.get_cell(r, c));
3526            }
3527            out.push(row_out);
3528        }
3529
3530        Ok(collapse_if_scalar(out, _ctx.date_system()))
3531    }
3532}
3533
3534pub fn register_builtins() {
3535    use crate::function_registry::register_function;
3536    use std::sync::Arc;
3537    register_function(Arc::new(XLookupFn));
3538    register_function(Arc::new(FilterFn));
3539    register_function(Arc::new(UniqueFn));
3540    register_function(Arc::new(SequenceFn));
3541    register_function(Arc::new(TransposeFn));
3542    register_function(Arc::new(TakeFn));
3543    register_function(Arc::new(DropFn));
3544    register_function(Arc::new(XMatchFn));
3545    register_function(Arc::new(SortFn));
3546    register_function(Arc::new(SortByFn));
3547    register_function(Arc::new(RandArrayFn));
3548    register_function(Arc::new(GroupByFn));
3549    register_function(Arc::new(PivotByFn));
3550}
3551
3552/* ───────────────────────── tests ───────────────────────── */
3553
3554#[cfg(test)]
3555mod tests {
3556    use super::*;
3557    use crate::test_workbook::TestWorkbook;
3558    use crate::traits::ArgumentHandle;
3559    use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
3560    use std::sync::Arc;
3561
3562    #[test]
3563    fn test_all_dynamic_functions_registered() {
3564        // Ensure builtins are registered
3565        crate::builtins::load_builtins();
3566
3567        let functions = [
3568            "XLOOKUP",
3569            "FILTER",
3570            "UNIQUE",
3571            "SEQUENCE",
3572            "TRANSPOSE",
3573            "TAKE",
3574            "DROP",
3575            "XMATCH",
3576            "SORT",
3577            "SORTBY",
3578            "RANDARRAY",
3579            "GROUPBY",
3580            "PIVOTBY",
3581        ];
3582
3583        for name in &functions {
3584            let result = crate::function_registry::get("", name);
3585            assert!(result.is_some(), "Function {} should be registered", name);
3586        }
3587    }
3588
3589    fn lit(v: LiteralValue) -> ASTNode {
3590        ASTNode::new(ASTNodeType::Literal(v), None)
3591    }
3592
3593    fn range(r: &str, sr: u32, sc: u32, er: u32, ec: u32) -> ASTNode {
3594        ASTNode::new(
3595            ASTNodeType::Reference {
3596                original: r.into(),
3597                reference: ReferenceType::range(None, Some(sr), Some(sc), Some(er), Some(ec)),
3598            },
3599            None,
3600        )
3601    }
3602
3603    #[test]
3604    fn xlookup_basic_exact_and_if_not_found() {
3605        let wb = TestWorkbook::new().with_function(Arc::new(XLookupFn));
3606        let wb = wb
3607            .with_cell_a1("Sheet1", "A1", LiteralValue::Text("a".into()))
3608            .with_cell_a1("Sheet1", "A2", LiteralValue::Text("b".into()))
3609            .with_cell_a1("Sheet1", "B1", LiteralValue::Int(10))
3610            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(20));
3611        let ctx = wb.interpreter();
3612        let lookup_range = range("A1:A2", 1, 1, 2, 1);
3613        let return_range = range("B1:B2", 1, 2, 2, 2);
3614        let f = ctx.context.get_function("", "XLOOKUP").unwrap();
3615        let key_b = lit(LiteralValue::Text("b".into()));
3616        let args = vec![
3617            ArgumentHandle::new(&key_b, &ctx),
3618            ArgumentHandle::new(&lookup_range, &ctx),
3619            ArgumentHandle::new(&return_range, &ctx),
3620        ];
3621        let v = f
3622            .dispatch(&args, &ctx.function_context(None))
3623            .unwrap()
3624            .into_literal();
3625        assert_eq!(v, LiteralValue::Number(20.0));
3626        let key_missing = lit(LiteralValue::Text("z".into()));
3627        let if_nf = lit(LiteralValue::Text("NF".into()));
3628        let args_nf = vec![
3629            ArgumentHandle::new(&key_missing, &ctx),
3630            ArgumentHandle::new(&lookup_range, &ctx),
3631            ArgumentHandle::new(&return_range, &ctx),
3632            ArgumentHandle::new(&if_nf, &ctx),
3633        ];
3634        let v_nf = f
3635            .dispatch(&args_nf, &ctx.function_context(None))
3636            .unwrap()
3637            .into_literal();
3638        assert_eq!(v_nf, LiteralValue::Text("NF".into()));
3639    }
3640
3641    #[test]
3642    fn xlookup_match_modes_next_smaller_larger() {
3643        let wb = TestWorkbook::new().with_function(Arc::new(XLookupFn));
3644        let wb = wb
3645            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(10))
3646            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(20))
3647            .with_cell_a1("Sheet1", "A3", LiteralValue::Int(30))
3648            .with_cell_a1("Sheet1", "B1", LiteralValue::Int(1))
3649            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(2))
3650            .with_cell_a1("Sheet1", "B3", LiteralValue::Int(3));
3651        let ctx = wb.interpreter();
3652        let lookup_range = range("A1:A3", 1, 1, 3, 1);
3653        let return_range = range("B1:B3", 1, 2, 3, 2);
3654        let f = ctx.context.get_function("", "XLOOKUP").unwrap();
3655        let needle_25 = lit(LiteralValue::Int(25));
3656        let mm_next_smaller = lit(LiteralValue::Int(-1));
3657        let nf_text = lit(LiteralValue::Text("NF".into()));
3658        let args_smaller = vec![
3659            ArgumentHandle::new(&needle_25, &ctx),
3660            ArgumentHandle::new(&lookup_range, &ctx),
3661            ArgumentHandle::new(&return_range, &ctx),
3662            ArgumentHandle::new(&nf_text, &ctx),
3663            ArgumentHandle::new(&mm_next_smaller, &ctx),
3664        ];
3665        let v_smaller = f
3666            .dispatch(&args_smaller, &ctx.function_context(None))
3667            .unwrap()
3668            .into_literal();
3669        assert_eq!(v_smaller, LiteralValue::Number(2.0));
3670        let mm_next_larger = lit(LiteralValue::Int(1));
3671        let nf_text2 = lit(LiteralValue::Text("NF".into()));
3672        let args_larger = vec![
3673            ArgumentHandle::new(&needle_25, &ctx),
3674            ArgumentHandle::new(&lookup_range, &ctx),
3675            ArgumentHandle::new(&return_range, &ctx),
3676            ArgumentHandle::new(&nf_text2, &ctx),
3677            ArgumentHandle::new(&mm_next_larger, &ctx),
3678        ];
3679        let v_larger = f
3680            .dispatch(&args_larger, &ctx.function_context(None))
3681            .unwrap()
3682            .into_literal();
3683        assert_eq!(v_larger, LiteralValue::Number(3.0));
3684    }
3685
3686    #[test]
3687    fn xlookup_wildcard_and_not_found_default_na() {
3688        let wb = TestWorkbook::new().with_function(Arc::new(XLookupFn));
3689        let wb = wb
3690            .with_cell_a1("Sheet1", "A1", LiteralValue::Text("Alpha".into()))
3691            .with_cell_a1("Sheet1", "A2", LiteralValue::Text("Beta".into()))
3692            .with_cell_a1("Sheet1", "A3", LiteralValue::Text("Gamma".into()))
3693            .with_cell_a1("Sheet1", "B1", LiteralValue::Int(100))
3694            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(200))
3695            .with_cell_a1("Sheet1", "B3", LiteralValue::Int(300));
3696        let ctx = wb.interpreter();
3697        let lookup_range = range("A1:A3", 1, 1, 3, 1);
3698        let return_range = range("B1:B3", 1, 2, 3, 2);
3699        let f = ctx.context.get_function("", "XLOOKUP").unwrap();
3700        // Wildcard should match Beta (*et*) with match_mode 2
3701        let pattern = lit(LiteralValue::Text("*et*".into()));
3702        let match_mode_wild = lit(LiteralValue::Int(2));
3703        let nf_binding = lit(LiteralValue::Text("NF".into()));
3704        let args_wild = vec![
3705            ArgumentHandle::new(&pattern, &ctx),
3706            ArgumentHandle::new(&lookup_range, &ctx),
3707            ArgumentHandle::new(&return_range, &ctx),
3708            ArgumentHandle::new(&nf_binding, &ctx),
3709            ArgumentHandle::new(&match_mode_wild, &ctx),
3710        ];
3711        let v_wild = f
3712            .dispatch(&args_wild, &ctx.function_context(None))
3713            .unwrap()
3714            .into_literal();
3715        assert_eq!(v_wild, LiteralValue::Number(200.0));
3716        // Escaped wildcard literal ~* should not match Beta
3717        let pattern_lit_star = lit(LiteralValue::Text("~*eta".into()));
3718        let args_lit = vec![
3719            ArgumentHandle::new(&pattern_lit_star, &ctx),
3720            ArgumentHandle::new(&lookup_range, &ctx),
3721            ArgumentHandle::new(&return_range, &ctx),
3722            ArgumentHandle::new(&nf_binding, &ctx),
3723            ArgumentHandle::new(&match_mode_wild, &ctx),
3724        ];
3725        let v_lit = f
3726            .dispatch(&args_lit, &ctx.function_context(None))
3727            .unwrap()
3728            .into_literal();
3729        match v_lit {
3730            LiteralValue::Text(s) => assert_eq!(s, "NF"),
3731            other => panic!("expected NF text got {other:?}"),
3732        }
3733        // Not found without if_not_found -> #N/A
3734        let missing = lit(LiteralValue::Text("Zeta".into()));
3735        let args_nf = vec![
3736            ArgumentHandle::new(&missing, &ctx),
3737            ArgumentHandle::new(&lookup_range, &ctx),
3738            ArgumentHandle::new(&return_range, &ctx),
3739        ];
3740        let v_nf = f
3741            .dispatch(&args_nf, &ctx.function_context(None))
3742            .unwrap()
3743            .into_literal();
3744        match v_nf {
3745            LiteralValue::Error(e) => assert_eq!(e.kind, ExcelErrorKind::Na),
3746            other => panic!("expected #N/A got {other:?}"),
3747        }
3748    }
3749
3750    #[test]
3751    fn xlookup_reverse_search_mode_picks_last() {
3752        let wb = TestWorkbook::new().with_function(Arc::new(XLookupFn));
3753        let wb = wb
3754            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(1))
3755            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(2))
3756            .with_cell_a1("Sheet1", "A3", LiteralValue::Int(1))
3757            .with_cell_a1("Sheet1", "B1", LiteralValue::Text("First".into()))
3758            .with_cell_a1("Sheet1", "B2", LiteralValue::Text("Mid".into()))
3759            .with_cell_a1("Sheet1", "B3", LiteralValue::Text("Last".into()));
3760        let ctx = wb.interpreter();
3761        let lookup_range = range("A1:A3", 1, 1, 3, 1);
3762        let return_range = range("B1:B3", 1, 2, 3, 2);
3763        let f = ctx.context.get_function("", "XLOOKUP").unwrap();
3764        let needle_one = lit(LiteralValue::Int(1));
3765        let search_rev = lit(LiteralValue::Int(-1));
3766        let nf_binding2 = lit(LiteralValue::Text("NF".into()));
3767        let match_mode_zero = lit(LiteralValue::Int(0));
3768        let args_rev = vec![
3769            ArgumentHandle::new(&needle_one, &ctx),
3770            ArgumentHandle::new(&lookup_range, &ctx),
3771            ArgumentHandle::new(&return_range, &ctx),
3772            ArgumentHandle::new(&nf_binding2, &ctx),
3773            /* match_mode default */ ArgumentHandle::new(&match_mode_zero, &ctx),
3774            ArgumentHandle::new(&search_rev, &ctx),
3775        ];
3776        let v_rev = f
3777            .dispatch(&args_rev, &ctx.function_context(None))
3778            .unwrap()
3779            .into_literal();
3780        assert_eq!(v_rev, LiteralValue::Text("Last".into()));
3781    }
3782
3783    #[test]
3784    fn xlookup_horizontal_returns_column_vector_for_matrix_return() {
3785        let wb = TestWorkbook::new().with_function(Arc::new(XLookupFn));
3786        let wb = wb
3787            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(10))
3788            .with_cell_a1("Sheet1", "B1", LiteralValue::Int(20))
3789            .with_cell_a1("Sheet1", "C1", LiteralValue::Int(30))
3790            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(1))
3791            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(2))
3792            .with_cell_a1("Sheet1", "C2", LiteralValue::Int(3))
3793            .with_cell_a1("Sheet1", "A3", LiteralValue::Int(4))
3794            .with_cell_a1("Sheet1", "B3", LiteralValue::Int(5))
3795            .with_cell_a1("Sheet1", "C3", LiteralValue::Int(6));
3796        let ctx = wb.interpreter();
3797        let lookup_range = range("A1:C1", 1, 1, 1, 3);
3798        let return_range = range("A2:C3", 2, 1, 3, 3);
3799        let f = ctx.context.get_function("", "XLOOKUP").unwrap();
3800        let needle = lit(LiteralValue::Int(20));
3801        let args = vec![
3802            ArgumentHandle::new(&needle, &ctx),
3803            ArgumentHandle::new(&lookup_range, &ctx),
3804            ArgumentHandle::new(&return_range, &ctx),
3805        ];
3806        let v = f
3807            .dispatch(&args, &ctx.function_context(None))
3808            .unwrap()
3809            .into_literal();
3810        match v {
3811            LiteralValue::Array(a) => {
3812                assert_eq!(
3813                    a,
3814                    vec![
3815                        vec![LiteralValue::Number(2.0)],
3816                        vec![LiteralValue::Number(5.0)]
3817                    ]
3818                );
3819            }
3820            other => panic!("expected array got {other:?}"),
3821        }
3822    }
3823
3824    #[test]
3825    fn xlookup_vertical_returns_row_vector_for_matrix_return() {
3826        let wb = TestWorkbook::new().with_function(Arc::new(XLookupFn));
3827        let wb = wb
3828            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(10))
3829            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(20))
3830            .with_cell_a1("Sheet1", "A3", LiteralValue::Int(30))
3831            .with_cell_a1("Sheet1", "B1", LiteralValue::Int(101))
3832            .with_cell_a1("Sheet1", "C1", LiteralValue::Int(102))
3833            .with_cell_a1("Sheet1", "D1", LiteralValue::Int(103))
3834            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(201))
3835            .with_cell_a1("Sheet1", "C2", LiteralValue::Int(202))
3836            .with_cell_a1("Sheet1", "D2", LiteralValue::Int(203))
3837            .with_cell_a1("Sheet1", "B3", LiteralValue::Int(301))
3838            .with_cell_a1("Sheet1", "C3", LiteralValue::Int(302))
3839            .with_cell_a1("Sheet1", "D3", LiteralValue::Int(303));
3840        let ctx = wb.interpreter();
3841        let lookup_range = range("A1:A3", 1, 1, 3, 1);
3842        let return_range = range("B1:D3", 1, 2, 3, 4);
3843        let f = ctx.context.get_function("", "XLOOKUP").unwrap();
3844        let needle = lit(LiteralValue::Int(20));
3845        let args = vec![
3846            ArgumentHandle::new(&needle, &ctx),
3847            ArgumentHandle::new(&lookup_range, &ctx),
3848            ArgumentHandle::new(&return_range, &ctx),
3849        ];
3850        let v = f
3851            .dispatch(&args, &ctx.function_context(None))
3852            .unwrap()
3853            .into_literal();
3854        match v {
3855            LiteralValue::Array(a) => {
3856                assert_eq!(
3857                    a,
3858                    vec![vec![
3859                        LiteralValue::Number(201.0),
3860                        LiteralValue::Number(202.0),
3861                        LiteralValue::Number(203.0)
3862                    ]]
3863                );
3864            }
3865            other => panic!("expected array got {other:?}"),
3866        }
3867    }
3868
3869    #[test]
3870    fn filter_basic_and_if_empty() {
3871        let wb = TestWorkbook::new().with_function(Arc::new(FilterFn));
3872        let wb = wb
3873            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(1))
3874            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(2))
3875            .with_cell_a1("Sheet1", "B1", LiteralValue::Int(10))
3876            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(20))
3877            .with_cell_a1("Sheet1", "C1", LiteralValue::Boolean(true))
3878            .with_cell_a1("Sheet1", "C2", LiteralValue::Boolean(false));
3879        let ctx = wb.interpreter();
3880        let array_range = range("A1:B2", 1, 1, 2, 2);
3881        let include_range = range("C1:C2", 1, 3, 2, 3);
3882        let f = ctx.context.get_function("", "FILTER").unwrap();
3883        let args = vec![
3884            ArgumentHandle::new(&array_range, &ctx),
3885            ArgumentHandle::new(&include_range, &ctx),
3886        ];
3887        let v = f
3888            .dispatch(&args, &ctx.function_context(None))
3889            .unwrap()
3890            .into_literal();
3891        match v {
3892            LiteralValue::Array(a) => {
3893                assert_eq!(a.len(), 1);
3894                assert_eq!(
3895                    a[0],
3896                    vec![LiteralValue::Number(1.0), LiteralValue::Number(10.0)]
3897                );
3898            }
3899            other => panic!("expected array got {other:?}"),
3900        }
3901        let wb2 = wb
3902            .with_cell_a1("Sheet1", "C1", LiteralValue::Boolean(false))
3903            .with_cell_a1("Sheet1", "C2", LiteralValue::Boolean(false));
3904        let ctx2 = wb2.interpreter();
3905        let f2 = ctx2.context.get_function("", "FILTER").unwrap();
3906        let empty_text = lit(LiteralValue::Text("EMPTY".into()));
3907        let args_empty = vec![
3908            ArgumentHandle::new(&array_range, &ctx2),
3909            ArgumentHandle::new(&include_range, &ctx2),
3910            ArgumentHandle::new(&empty_text, &ctx2),
3911        ];
3912        let v_empty = f2
3913            .dispatch(&args_empty, &ctx2.function_context(None))
3914            .unwrap()
3915            .into_literal();
3916        assert_eq!(v_empty, LiteralValue::Text("EMPTY".into()));
3917    }
3918
3919    #[test]
3920    fn unique_basic_and_exactly_once() {
3921        let wb = TestWorkbook::new().with_function(Arc::new(UniqueFn));
3922        let wb = wb
3923            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(1))
3924            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(1))
3925            .with_cell_a1("Sheet1", "A3", LiteralValue::Int(2))
3926            .with_cell_a1("Sheet1", "A4", LiteralValue::Int(3));
3927        let ctx = wb.interpreter();
3928        let range = range("A1:A4", 1, 1, 4, 1);
3929        let f = ctx.context.get_function("", "UNIQUE").unwrap();
3930        let args = vec![ArgumentHandle::new(&range, &ctx)];
3931        let v = f
3932            .dispatch(&args, &ctx.function_context(None))
3933            .unwrap()
3934            .into_literal();
3935        match v {
3936            LiteralValue::Array(a) => {
3937                assert_eq!(a.len(), 3);
3938                assert_eq!(a[0][0], LiteralValue::Number(1.0));
3939            }
3940            _ => panic!("expected array"),
3941        }
3942    }
3943
3944    #[test]
3945    fn sequence_basic_rows_cols_step() {
3946        let wb = TestWorkbook::new().with_function(Arc::new(SequenceFn));
3947        let ctx = wb.interpreter();
3948        let f = ctx.context.get_function("", "SEQUENCE").unwrap();
3949        let rows = lit(LiteralValue::Int(2));
3950        let cols = lit(LiteralValue::Int(3));
3951        let start = lit(LiteralValue::Int(5));
3952        let step = lit(LiteralValue::Int(2));
3953        let args = vec![
3954            ArgumentHandle::new(&rows, &ctx),
3955            ArgumentHandle::new(&cols, &ctx),
3956            ArgumentHandle::new(&start, &ctx),
3957            ArgumentHandle::new(&step, &ctx),
3958        ];
3959        let v = f
3960            .dispatch(&args, &ctx.function_context(None))
3961            .unwrap()
3962            .into_literal();
3963        match v {
3964            LiteralValue::Array(a) => {
3965                assert_eq!(a.len(), 2);
3966                assert_eq!(a[0][0], LiteralValue::Number(5.0));
3967            }
3968            other => panic!("expected array got {other:?}"),
3969        }
3970    }
3971
3972    #[test]
3973    fn transpose_basic() {
3974        let wb = TestWorkbook::new().with_function(Arc::new(TransposeFn));
3975        let wb = wb
3976            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(1))
3977            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(2))
3978            .with_cell_a1("Sheet1", "B1", LiteralValue::Int(10))
3979            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(20));
3980        let ctx = wb.interpreter();
3981        let arr = range("A1:B2", 1, 1, 2, 2);
3982        let f = ctx.context.get_function("", "TRANSPOSE").unwrap();
3983        let args = vec![ArgumentHandle::new(&arr, &ctx)];
3984        let v = f
3985            .dispatch(&args, &ctx.function_context(None))
3986            .unwrap()
3987            .into_literal();
3988        match v {
3989            LiteralValue::Array(a) => {
3990                assert_eq!(a.len(), 2);
3991                assert_eq!(
3992                    a[0],
3993                    vec![LiteralValue::Number(1.0), LiteralValue::Number(2.0)]
3994                );
3995            }
3996            other => panic!("expected array got {other:?}"),
3997        }
3998    }
3999
4000    #[test]
4001    fn take_basic() {
4002        let wb = TestWorkbook::new().with_function(Arc::new(TakeFn));
4003        let wb = wb
4004            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(1))
4005            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(2));
4006        let ctx = wb.interpreter();
4007        let arr = range("A1:A2", 1, 1, 2, 1);
4008        let f = ctx.context.get_function("", "TAKE").unwrap();
4009        let one = lit(LiteralValue::Int(1));
4010        let args = vec![
4011            ArgumentHandle::new(&arr, &ctx),
4012            ArgumentHandle::new(&one, &ctx),
4013        ];
4014        let v = f
4015            .dispatch(&args, &ctx.function_context(None))
4016            .unwrap()
4017            .into_literal();
4018        assert_eq!(v, LiteralValue::Number(1.0));
4019    }
4020
4021    #[test]
4022    fn drop_basic() {
4023        let wb = TestWorkbook::new().with_function(Arc::new(DropFn));
4024        let wb = wb
4025            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(1))
4026            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(2));
4027        let ctx = wb.interpreter();
4028        let arr = range("A1:A2", 1, 1, 2, 1);
4029        let f = ctx.context.get_function("", "DROP").unwrap();
4030        let one = lit(LiteralValue::Int(1));
4031        let args = vec![
4032            ArgumentHandle::new(&arr, &ctx),
4033            ArgumentHandle::new(&one, &ctx),
4034        ];
4035        let v = f
4036            .dispatch(&args, &ctx.function_context(None))
4037            .unwrap()
4038            .into_literal();
4039        assert_eq!(v, LiteralValue::Number(2.0));
4040    }
4041
4042    #[test]
4043    fn xmatch_exact_match_default() {
4044        let wb = TestWorkbook::new().with_function(Arc::new(XMatchFn));
4045        let wb = wb
4046            .with_cell_a1("Sheet1", "A1", LiteralValue::Text("apple".into()))
4047            .with_cell_a1("Sheet1", "A2", LiteralValue::Text("banana".into()))
4048            .with_cell_a1("Sheet1", "A3", LiteralValue::Text("cherry".into()));
4049        let ctx = wb.interpreter();
4050        let lookup_range = range("A1:A3", 1, 1, 3, 1);
4051        let f = ctx.context.get_function("", "XMATCH").unwrap();
4052        let key = lit(LiteralValue::Text("banana".into()));
4053        let args = vec![
4054            ArgumentHandle::new(&key, &ctx),
4055            ArgumentHandle::new(&lookup_range, &ctx),
4056        ];
4057        let v = f
4058            .dispatch(&args, &ctx.function_context(None))
4059            .unwrap()
4060            .into_literal();
4061        assert_eq!(v, LiteralValue::Int(2));
4062    }
4063
4064    #[test]
4065    fn xmatch_exact_or_next_smaller() {
4066        let wb = TestWorkbook::new().with_function(Arc::new(XMatchFn));
4067        let wb = wb
4068            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(10))
4069            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(20))
4070            .with_cell_a1("Sheet1", "A3", LiteralValue::Int(30));
4071        let ctx = wb.interpreter();
4072        let lookup_range = range("A1:A3", 1, 1, 3, 1);
4073        let f = ctx.context.get_function("", "XMATCH").unwrap();
4074        let needle = lit(LiteralValue::Int(25));
4075        let match_mode = lit(LiteralValue::Int(-1)); // exact or next smaller
4076        let args = vec![
4077            ArgumentHandle::new(&needle, &ctx),
4078            ArgumentHandle::new(&lookup_range, &ctx),
4079            ArgumentHandle::new(&match_mode, &ctx),
4080        ];
4081        let v = f
4082            .dispatch(&args, &ctx.function_context(None))
4083            .unwrap()
4084            .into_literal();
4085        assert_eq!(v, LiteralValue::Int(2)); // 20 is the largest <= 25
4086    }
4087
4088    #[test]
4089    fn xmatch_exact_or_next_larger() {
4090        let wb = TestWorkbook::new().with_function(Arc::new(XMatchFn));
4091        let wb = wb
4092            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(10))
4093            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(20))
4094            .with_cell_a1("Sheet1", "A3", LiteralValue::Int(30));
4095        let ctx = wb.interpreter();
4096        let lookup_range = range("A1:A3", 1, 1, 3, 1);
4097        let f = ctx.context.get_function("", "XMATCH").unwrap();
4098        let needle = lit(LiteralValue::Int(25));
4099        let match_mode = lit(LiteralValue::Int(1)); // exact or next larger
4100        let args = vec![
4101            ArgumentHandle::new(&needle, &ctx),
4102            ArgumentHandle::new(&lookup_range, &ctx),
4103            ArgumentHandle::new(&match_mode, &ctx),
4104        ];
4105        let v = f
4106            .dispatch(&args, &ctx.function_context(None))
4107            .unwrap()
4108            .into_literal();
4109        assert_eq!(v, LiteralValue::Int(3)); // 30 is the smallest >= 25
4110    }
4111
4112    #[test]
4113    fn xmatch_wildcard() {
4114        let wb = TestWorkbook::new().with_function(Arc::new(XMatchFn));
4115        let wb = wb
4116            .with_cell_a1("Sheet1", "A1", LiteralValue::Text("alpha".into()))
4117            .with_cell_a1("Sheet1", "A2", LiteralValue::Text("beta".into()))
4118            .with_cell_a1("Sheet1", "A3", LiteralValue::Text("gamma".into()));
4119        let ctx = wb.interpreter();
4120        let lookup_range = range("A1:A3", 1, 1, 3, 1);
4121        let f = ctx.context.get_function("", "XMATCH").unwrap();
4122        let pattern = lit(LiteralValue::Text("*eta".into()));
4123        let match_mode = lit(LiteralValue::Int(2)); // wildcard
4124        let args = vec![
4125            ArgumentHandle::new(&pattern, &ctx),
4126            ArgumentHandle::new(&lookup_range, &ctx),
4127            ArgumentHandle::new(&match_mode, &ctx),
4128        ];
4129        let v = f
4130            .dispatch(&args, &ctx.function_context(None))
4131            .unwrap()
4132            .into_literal();
4133        assert_eq!(v, LiteralValue::Int(2)); // "beta" matches "*eta"
4134    }
4135
4136    #[test]
4137    fn xmatch_reverse_search() {
4138        let wb = TestWorkbook::new().with_function(Arc::new(XMatchFn));
4139        let wb = wb
4140            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(1))
4141            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(2))
4142            .with_cell_a1("Sheet1", "A3", LiteralValue::Int(1)); // duplicate
4143        let ctx = wb.interpreter();
4144        let lookup_range = range("A1:A3", 1, 1, 3, 1);
4145        let f = ctx.context.get_function("", "XMATCH").unwrap();
4146        let needle = lit(LiteralValue::Int(1));
4147        let match_mode = lit(LiteralValue::Int(0));
4148        let search_mode = lit(LiteralValue::Int(-1)); // last to first
4149        let args = vec![
4150            ArgumentHandle::new(&needle, &ctx),
4151            ArgumentHandle::new(&lookup_range, &ctx),
4152            ArgumentHandle::new(&match_mode, &ctx),
4153            ArgumentHandle::new(&search_mode, &ctx),
4154        ];
4155        let v = f
4156            .dispatch(&args, &ctx.function_context(None))
4157            .unwrap()
4158            .into_literal();
4159        assert_eq!(v, LiteralValue::Int(3)); // last occurrence of 1
4160    }
4161
4162    #[test]
4163    fn xmatch_not_found() {
4164        let wb = TestWorkbook::new().with_function(Arc::new(XMatchFn));
4165        let wb = wb
4166            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(1))
4167            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(2))
4168            .with_cell_a1("Sheet1", "A3", LiteralValue::Int(3));
4169        let ctx = wb.interpreter();
4170        let lookup_range = range("A1:A3", 1, 1, 3, 1);
4171        let f = ctx.context.get_function("", "XMATCH").unwrap();
4172        let needle = lit(LiteralValue::Int(5));
4173        let args = vec![
4174            ArgumentHandle::new(&needle, &ctx),
4175            ArgumentHandle::new(&lookup_range, &ctx),
4176        ];
4177        let v = f
4178            .dispatch(&args, &ctx.function_context(None))
4179            .unwrap()
4180            .into_literal();
4181        match v {
4182            LiteralValue::Error(e) => assert_eq!(e.kind, ExcelErrorKind::Na),
4183            other => panic!("expected #N/A got {other:?}"),
4184        }
4185    }
4186
4187    #[test]
4188    fn sort_basic_ascending() {
4189        let wb = TestWorkbook::new().with_function(Arc::new(SortFn));
4190        let wb = wb
4191            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(30))
4192            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(10))
4193            .with_cell_a1("Sheet1", "A3", LiteralValue::Int(20));
4194        let ctx = wb.interpreter();
4195        let arr = range("A1:A3", 1, 1, 3, 1);
4196        let f = ctx.context.get_function("", "SORT").unwrap();
4197        let args = vec![ArgumentHandle::new(&arr, &ctx)];
4198        let v = f
4199            .dispatch(&args, &ctx.function_context(None))
4200            .unwrap()
4201            .into_literal();
4202        match v {
4203            LiteralValue::Array(a) => {
4204                assert_eq!(a.len(), 3);
4205                assert_eq!(a[0][0], LiteralValue::Number(10.0));
4206                assert_eq!(a[1][0], LiteralValue::Number(20.0));
4207                assert_eq!(a[2][0], LiteralValue::Number(30.0));
4208            }
4209            other => panic!("expected array got {other:?}"),
4210        }
4211    }
4212
4213    #[test]
4214    fn sort_descending() {
4215        let wb = TestWorkbook::new().with_function(Arc::new(SortFn));
4216        let wb = wb
4217            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(30))
4218            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(10))
4219            .with_cell_a1("Sheet1", "A3", LiteralValue::Int(20));
4220        let ctx = wb.interpreter();
4221        let arr = range("A1:A3", 1, 1, 3, 1);
4222        let f = ctx.context.get_function("", "SORT").unwrap();
4223        let sort_index = lit(LiteralValue::Int(1));
4224        let sort_order = lit(LiteralValue::Int(-1)); // descending
4225        let args = vec![
4226            ArgumentHandle::new(&arr, &ctx),
4227            ArgumentHandle::new(&sort_index, &ctx),
4228            ArgumentHandle::new(&sort_order, &ctx),
4229        ];
4230        let v = f
4231            .dispatch(&args, &ctx.function_context(None))
4232            .unwrap()
4233            .into_literal();
4234        match v {
4235            LiteralValue::Array(a) => {
4236                assert_eq!(a.len(), 3);
4237                assert_eq!(a[0][0], LiteralValue::Number(30.0));
4238                assert_eq!(a[1][0], LiteralValue::Number(20.0));
4239                assert_eq!(a[2][0], LiteralValue::Number(10.0));
4240            }
4241            other => panic!("expected array got {other:?}"),
4242        }
4243    }
4244
4245    #[test]
4246    fn sort_by_column() {
4247        let wb = TestWorkbook::new().with_function(Arc::new(SortFn));
4248        let wb = wb
4249            .with_cell_a1("Sheet1", "A1", LiteralValue::Text("Charlie".into()))
4250            .with_cell_a1("Sheet1", "B1", LiteralValue::Int(30))
4251            .with_cell_a1("Sheet1", "A2", LiteralValue::Text("Alice".into()))
4252            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(10))
4253            .with_cell_a1("Sheet1", "A3", LiteralValue::Text("Bob".into()))
4254            .with_cell_a1("Sheet1", "B3", LiteralValue::Int(20));
4255        let ctx = wb.interpreter();
4256        let arr = range("A1:B3", 1, 1, 3, 2);
4257        let f = ctx.context.get_function("", "SORT").unwrap();
4258        let sort_index = lit(LiteralValue::Int(2)); // sort by column B
4259        let args = vec![
4260            ArgumentHandle::new(&arr, &ctx),
4261            ArgumentHandle::new(&sort_index, &ctx),
4262        ];
4263        let v = f
4264            .dispatch(&args, &ctx.function_context(None))
4265            .unwrap()
4266            .into_literal();
4267        match v {
4268            LiteralValue::Array(a) => {
4269                assert_eq!(a.len(), 3);
4270                // Should be sorted by column B: Alice(10), Bob(20), Charlie(30)
4271                assert_eq!(a[0][0], LiteralValue::Text("Alice".into()));
4272                assert_eq!(a[1][0], LiteralValue::Text("Bob".into()));
4273                assert_eq!(a[2][0], LiteralValue::Text("Charlie".into()));
4274            }
4275            other => panic!("expected array got {other:?}"),
4276        }
4277    }
4278
4279    #[test]
4280    fn sortby_basic() {
4281        let wb = TestWorkbook::new().with_function(Arc::new(SortByFn));
4282        let wb = wb
4283            .with_cell_a1("Sheet1", "A1", LiteralValue::Text("Charlie".into()))
4284            .with_cell_a1("Sheet1", "A2", LiteralValue::Text("Alice".into()))
4285            .with_cell_a1("Sheet1", "A3", LiteralValue::Text("Bob".into()))
4286            .with_cell_a1("Sheet1", "B1", LiteralValue::Int(3))
4287            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(1))
4288            .with_cell_a1("Sheet1", "B3", LiteralValue::Int(2));
4289        let ctx = wb.interpreter();
4290        let arr = range("A1:A3", 1, 1, 3, 1);
4291        let by_arr = range("B1:B3", 1, 2, 3, 2);
4292        let f = ctx.context.get_function("", "SORTBY").unwrap();
4293        let args = vec![
4294            ArgumentHandle::new(&arr, &ctx),
4295            ArgumentHandle::new(&by_arr, &ctx),
4296        ];
4297        let v = f
4298            .dispatch(&args, &ctx.function_context(None))
4299            .unwrap()
4300            .into_literal();
4301        match v {
4302            LiteralValue::Array(a) => {
4303                assert_eq!(a.len(), 3);
4304                // Should be sorted by B values: Alice(1), Bob(2), Charlie(3)
4305                assert_eq!(a[0][0], LiteralValue::Text("Alice".into()));
4306                assert_eq!(a[1][0], LiteralValue::Text("Bob".into()));
4307                assert_eq!(a[2][0], LiteralValue::Text("Charlie".into()));
4308            }
4309            other => panic!("expected array got {other:?}"),
4310        }
4311    }
4312
4313    #[test]
4314    fn sortby_descending() {
4315        let wb = TestWorkbook::new().with_function(Arc::new(SortByFn));
4316        let wb = wb
4317            .with_cell_a1("Sheet1", "A1", LiteralValue::Text("Charlie".into()))
4318            .with_cell_a1("Sheet1", "A2", LiteralValue::Text("Alice".into()))
4319            .with_cell_a1("Sheet1", "A3", LiteralValue::Text("Bob".into()))
4320            .with_cell_a1("Sheet1", "B1", LiteralValue::Int(3))
4321            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(1))
4322            .with_cell_a1("Sheet1", "B3", LiteralValue::Int(2));
4323        let ctx = wb.interpreter();
4324        let arr = range("A1:A3", 1, 1, 3, 1);
4325        let by_arr = range("B1:B3", 1, 2, 3, 2);
4326        let sort_order = lit(LiteralValue::Int(-1)); // descending
4327        let f = ctx.context.get_function("", "SORTBY").unwrap();
4328        let args = vec![
4329            ArgumentHandle::new(&arr, &ctx),
4330            ArgumentHandle::new(&by_arr, &ctx),
4331            ArgumentHandle::new(&sort_order, &ctx),
4332        ];
4333        let v = f
4334            .dispatch(&args, &ctx.function_context(None))
4335            .unwrap()
4336            .into_literal();
4337        match v {
4338            LiteralValue::Array(a) => {
4339                assert_eq!(a.len(), 3);
4340                // Should be sorted by B values descending: Charlie(3), Bob(2), Alice(1)
4341                assert_eq!(a[0][0], LiteralValue::Text("Charlie".into()));
4342                assert_eq!(a[1][0], LiteralValue::Text("Bob".into()));
4343                assert_eq!(a[2][0], LiteralValue::Text("Alice".into()));
4344            }
4345            other => panic!("expected array got {other:?}"),
4346        }
4347    }
4348
4349    #[test]
4350    fn randarray_basic() {
4351        let wb = TestWorkbook::new().with_function(Arc::new(RandArrayFn));
4352        let ctx = wb.interpreter();
4353        let f = ctx.context.get_function("", "RANDARRAY").unwrap();
4354
4355        // Test basic 2x3 array with defaults
4356        let rows = lit(LiteralValue::Int(2));
4357        let cols = lit(LiteralValue::Int(3));
4358        let args = vec![
4359            ArgumentHandle::new(&rows, &ctx),
4360            ArgumentHandle::new(&cols, &ctx),
4361        ];
4362        let v = f
4363            .dispatch(&args, &ctx.function_context(None))
4364            .unwrap()
4365            .into_literal();
4366        match v {
4367            LiteralValue::Array(a) => {
4368                assert_eq!(a.len(), 2);
4369                assert_eq!(a[0].len(), 3);
4370                // Check all values are between 0 and 1
4371                for row in &a {
4372                    for cell in row {
4373                        match cell {
4374                            LiteralValue::Number(n) => {
4375                                assert!(*n >= 0.0 && *n < 1.0, "Value {n} not in [0, 1)");
4376                            }
4377                            other => panic!("expected Number got {other:?}"),
4378                        }
4379                    }
4380                }
4381            }
4382            other => panic!("expected array got {other:?}"),
4383        }
4384    }
4385
4386    #[test]
4387    fn randarray_whole_numbers() {
4388        let wb = TestWorkbook::new().with_function(Arc::new(RandArrayFn));
4389        let ctx = wb.interpreter();
4390        let f = ctx.context.get_function("", "RANDARRAY").unwrap();
4391
4392        // Test 3x2 array with whole numbers between 1 and 10
4393        let rows = lit(LiteralValue::Int(3));
4394        let cols = lit(LiteralValue::Int(2));
4395        let min = lit(LiteralValue::Int(1));
4396        let max = lit(LiteralValue::Int(10));
4397        let whole = lit(LiteralValue::Boolean(true));
4398        let args = vec![
4399            ArgumentHandle::new(&rows, &ctx),
4400            ArgumentHandle::new(&cols, &ctx),
4401            ArgumentHandle::new(&min, &ctx),
4402            ArgumentHandle::new(&max, &ctx),
4403            ArgumentHandle::new(&whole, &ctx),
4404        ];
4405        let v = f
4406            .dispatch(&args, &ctx.function_context(None))
4407            .unwrap()
4408            .into_literal();
4409        match v {
4410            LiteralValue::Array(a) => {
4411                assert_eq!(a.len(), 3);
4412                assert_eq!(a[0].len(), 2);
4413                // Check all values are integers between 1 and 10
4414                for row in &a {
4415                    for cell in row {
4416                        let n = match cell {
4417                            LiteralValue::Int(n) => *n as f64,
4418                            LiteralValue::Number(n) => *n,
4419                            other => panic!("expected Int or Number got {other:?}"),
4420                        };
4421                        assert!((1.0..=10.0).contains(&n), "Value {n} not in [1, 10]");
4422                        // Verify it's actually a whole number
4423                        assert!(n.fract() == 0.0, "Value {n} is not a whole number");
4424                    }
4425                }
4426            }
4427            other => panic!("expected array got {other:?}"),
4428        }
4429    }
4430}