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