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