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