Skip to main content

visi_core/core/engine/sheet/
mod.rs

1//! The `Sheet` type: a worksheet's cells, its formula evaluation, and the
2//! dependency-tracked recalculation over them.
3
4mod edit;
5mod functions;
6
7pub use edit::get_word_boundaries_from_str;
8
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, HashSet, VecDeque};
11use web_time::Instant;
12
13use super::cell::{CellRef, Dependency, EngineError, EvalError, TextCellRef, generate_unique_id};
14use super::column::DataColumn;
15use super::result_data::ResultData;
16/// Context for evaluating expressions, containing references to other sheets
17#[derive(Default)]
18pub struct Context<'a> {
19    /// Map of sheet names to sheet references for cross-sheet lookups
20    pub sheets: HashMap<String, &'a Sheet>,
21    /// Every pivot table in the workbook, so `GETPIVOTDATA` can resolve a
22    /// rendered pivot's destination cell back to its definition. Pivot
23    /// tables are workbook-level (like `Context.sheets`' cross-sheet
24    /// lookups), not sheet-scoped, so this lives here rather than on
25    /// `Sheet` itself.
26    pub pivot_tables: &'a [crate::core::pivot::PivotTable],
27    /// Sheet names in true workbook order, so `SHEET()` can report a real
28    /// ordinal. `sheets` is an unordered `HashMap`, which is why this is
29    /// tracked separately rather than derived from it -- true order only
30    /// exists one layer up, in `visi`'s `WorkbookManager::sheets` (a
31    /// `Vec`), which populates this when building the context.
32    pub sheet_order: Vec<String>,
33}
34
35impl<'a> Context<'a> {
36    /// Create a new empty context
37    pub fn new() -> Self {
38        Self {
39            sheets: HashMap::new(),
40            pivot_tables: &[],
41            sheet_order: Vec::new(),
42        }
43    }
44
45    /// Add a sheet to the context for lookup during evaluation
46    pub fn add_table(&mut self, name: String, sheet: &'a Sheet) {
47        self.sheets.insert(name, sheet);
48    }
49}
50
51/// A chain of LET name/value bindings in scope while evaluating a single
52/// formula. This is a linked list (not a cloned `HashMap`) because LET
53/// binds names one at a time -- each value expression, and the final
54/// calculation, must see all *earlier* bindings from the same LET (and any
55/// outer LET it's nested inside), and a name can shadow an outer binding of
56/// the same spelling. `evaluate_let` builds this chain by recursing one
57/// pair at a time rather than mutating a shared map.
58enum LetScope<'a> {
59    Empty,
60    Bound {
61        name: &'a str,
62        value: &'a ResultData,
63        parent: &'a LetScope<'a>,
64    },
65}
66
67impl<'a> LetScope<'a> {
68    fn get(&self, name: &str) -> Option<&ResultData> {
69        match self {
70            LetScope::Empty => None,
71            LetScope::Bound {
72                name: n,
73                value,
74                parent,
75            } => {
76                if n.eq_ignore_ascii_case(name) {
77                    Some(value)
78                } else {
79                    parent.get(name)
80                }
81            }
82        }
83    }
84}
85
86/// Which way a fill or selection extends from its anchor cell.
87#[derive(Debug, Clone, Copy, PartialEq)]
88pub enum Direction {
89    /// No direction; the operation is a no-op.
90    None,
91    /// Toward row 0.
92    Up,
93    /// Toward the last row.
94    Down,
95    /// Toward column 0.
96    Left,
97    /// Toward the last column.
98    Right,
99}
100
101/// One worksheet: a grid of cells, the formulas over them, and the dependency
102/// graph that keeps them up to date.
103///
104/// # Coordinates
105///
106/// Everything here is **0-based `(row, col)`**. A1 notation exists only at the
107/// parser and CLI boundaries -- see [`parse_a1_coordinates`] and
108/// [`col_idx_to_letters`] to convert.
109///
110/// # Naming trap
111///
112/// A `Sheet` is informally called a "table" in places (a new one is named
113/// `table_1`, and `Context::add_table` registers one). That is *not* an
114/// [`ExcelTable`], which is a ListObject -- a named rectangular range *on* a
115/// sheet -- and lives in [`Sheet::tables`].
116///
117/// # Storage
118///
119/// Storage is column-oriented: each [`DataColumn`] keeps the raw user text,
120/// the computed values and the compiled formulas in three parallel vectors
121/// that must stay the same length. The row and column insert/delete paths
122/// maintain that invariant by hand, so a new one has to do the same.
123///
124/// # Recalculation
125///
126/// [`Sheet::commit`] recomputes the dirty cells and propagates through
127/// [`Dependency::Local`] and [`Dependency::LocalColumn`] edges only.
128/// Cross-sheet edges are `WorkbookManager::evaluate`'s job, and evaluating a
129/// formula with a remote reference requires a [`Context`] -- without one it
130/// errors.
131///
132/// [`parse_a1_coordinates`]: crate::core::parse_a1_coordinates
133/// [`col_idx_to_letters`]: crate::core::col_idx_to_letters
134/// [`ExcelTable`]: crate::core::table::ExcelTable
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct Sheet {
137    /// Workbook-unique identifier. Formulas compile references against this
138    /// rather than the name, which is what makes a rename non-destructive.
139    #[serde(default = "generate_unique_id")]
140    pub id: u64,
141    /// Display name, as it appears in a cross-sheet reference.
142    pub name: String,
143    /// The cells, one entry per column. Row `r` of column `c` is
144    /// `columns[c]`'s entry `r`.
145    ///
146    /// Every column has the same number of rows -- [`Sheet::row_count`] reads
147    /// only the first and assumes the rest match -- so the `Vec` itself is
148    /// crate-private. Read them through [`Sheet::columns`].
149    pub(crate) columns: Vec<DataColumn>,
150    /// Excel Tables (ListObjects) defined on this sheet.
151    #[serde(default)]
152    pub tables: Vec<crate::core::table::ExcelTable>,
153    /// Forward edges: which cells must be recomputed when a dependency
154    /// changes. Rebuilt from the formulas, so not serialized.
155    #[serde(skip, default)]
156    pub dependencies: HashMap<Dependency, HashSet<CellRef>>,
157    /// Reverse edges: what each cell currently reads, so its old edges can be
158    /// dropped when its formula changes. Rebuilt, so not serialized.
159    #[serde(skip, default)]
160    pub dependencies_rev: HashMap<CellRef, HashSet<Dependency>>,
161    /// Edits made since the last commit, for callers that want to observe or
162    /// replay them.
163    #[serde(skip)]
164    pub uncommitted_actions: Vec<crate::core::SheetAction>,
165}
166
167/// Arguments for [`Sheet::new`]. [`Default`] gives a 10x5 sheet with a
168/// generated id and the name `table_1`.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct SheetInit {
171    /// Identifier to use; `None` generates a fresh one.
172    #[serde(default)]
173    pub id: Option<u64>,
174    /// Name to use; `None` means `table_1`.
175    pub name: Option<String>,
176    /// Rows to allocate.
177    pub rows: usize,
178    /// Columns to allocate.
179    pub cols: usize,
180}
181
182impl Default for SheetInit {
183    fn default() -> Self {
184        Self {
185            id: None,
186            name: None,
187            rows: 10,
188            cols: 5,
189        }
190    }
191}
192
193/// How a blank cell is treated by the strict numeric flatteners.
194#[derive(Clone, Copy, PartialEq, Eq)]
195enum BlankPolicy {
196    /// Counts as 0 (MULTINOMIAL).
197    Zero,
198    /// Dropped entirely, shifting later elements (SERIESSUM).
199    Skip,
200    /// #VALUE!, like text (LINEST/TREND/GROWTH/LOGEST/MMULT).
201    Reject,
202}
203
204impl Sheet {
205    /// Creates a sheet of `args.rows` x `args.cols` empty cells, every one of
206    /// them queued as a pending edit so the first [`Sheet::commit`] sees them.
207    pub fn new(args: SheetInit) -> Sheet {
208        let SheetInit {
209            id,
210            name,
211            rows,
212            cols,
213        } = args;
214        let sheet_id = id.unwrap_or_else(generate_unique_id);
215        let sheet_name = name.unwrap_or_else(|| "table_1".to_string());
216
217        let mut columns = Vec::with_capacity(cols);
218        for _ in 0..cols {
219            columns.push(DataColumn::new(rows));
220        }
221
222        let mut uncommitted_actions = Vec::new();
223        for c in 0..cols {
224            for r in 0..rows {
225                uncommitted_actions.push(crate::core::SheetAction::SetCellSrc {
226                    sheet_name: sheet_name.clone(),
227                    col: c,
228                    row: r,
229                    src: String::new(),
230                });
231            }
232        }
233
234        Self {
235            id: sheet_id,
236            name: sheet_name,
237            columns,
238            tables: Vec::new(),
239            dependencies: HashMap::new(),
240            dependencies_rev: HashMap::new(),
241            uncommitted_actions,
242        }
243    }
244
245    /// Rebuilds what serialization drops.
246    ///
247    /// Only the raw source text is persisted, so this resizes the value and
248    /// compiled-formula vectors back to match it -- restoring the
249    /// same-length invariant -- and marks everything dirty. Call it after
250    /// deserializing, before [`Sheet::commit`].
251    pub fn setup_after_deserialization(&mut self) {
252        for col in &mut self.columns {
253            col.rebuild_after_load();
254        }
255        self.mark_all_dirty();
256    }
257
258    /// Every sheet a formula on this one could refer to -- this sheet first,
259    /// then the rest of `context` -- as the name-to-id lookup table that
260    /// `compile_formula` resolves references against.
261    ///
262    /// "Tables" here means sheets, not [`ExcelTable`]s.
263    ///
264    /// [`ExcelTable`]: crate::core::table::ExcelTable
265    pub(crate) fn get_all_tables_for_compilation(&self, context: Option<&Context>) -> Vec<Sheet> {
266        let mut list = vec![self.clone()];
267        let mut seen = std::collections::HashSet::new();
268        seen.insert(self.id);
269        if let Some(ctx) = context {
270            for t in ctx.sheets.values() {
271                if !seen.contains(&t.id) {
272                    seen.insert(t.id);
273                    list.push((*t).clone());
274                }
275            }
276        }
277        list
278    }
279
280    /// Queues every cell for recomputation on the next [`Sheet::commit`].
281    ///
282    /// This is how cross-sheet staleness is handled: `WorkbookManager` cannot
283    /// tell which cells a remote edit reached, so it marks whole sheets.
284    pub fn mark_all_dirty(&mut self) {
285        for col in &mut self.columns {
286            col.dirty_indices.clear();
287            col.dirty_indices.extend(0..col.src.len());
288        }
289    }
290
291    /// Commit all changed src items with a context for sheet lookups
292    pub fn commit(&mut self, context: Option<&Context>) -> Result<HashSet<CellRef>, EngineError> {
293        let mut queue: VecDeque<CellRef> = VecDeque::new();
294        let mut queue_set: HashSet<CellRef> = HashSet::new();
295        let mut updated_cells: HashSet<CellRef> = HashSet::new();
296
297        // 1. Collect initial dirty cells
298        for (col_idx, col_data) in self.columns.iter_mut().enumerate() {
299            for row_idx in &col_data.dirty_indices {
300                let cell = CellRef::new(*row_idx, col_idx);
301                queue.push_back(cell);
302                queue_set.insert(cell);
303                updated_cells.insert(cell);
304            }
305            col_data.dirty_indices.clear();
306        }
307
308        let initial_queue_len = queue.len();
309        if initial_queue_len == 0 {
310            return Ok(updated_cells);
311        }
312
313        let start_commit = Instant::now();
314        log::info!(
315            "Sheet '{}' commit starting for {} dirty cells",
316            self.name,
317            initial_queue_len
318        );
319        let max_ops = 10000.max(initial_queue_len * 3);
320        let mut ops = 0;
321
322        let mut tables_for_compilation = self.get_all_tables_for_compilation(context);
323        let mut last_log_time = Instant::now();
324
325        while let Some(cell_ref) = queue.pop_front() {
326            queue_set.remove(&cell_ref);
327            ops += 1;
328            if ops > max_ops {
329                println!("Circular dependency or too many updates detected");
330                break;
331            }
332
333            if ops % 50000 == 0 {
334                log::info!(
335                    "Sheet '{}' commit progress: {}/{} cells processed ({:.2?})",
336                    self.name,
337                    ops,
338                    initial_queue_len,
339                    last_log_time.elapsed()
340                );
341                last_log_time = Instant::now();
342            }
343
344            // `Some(code)` when this cell's literal was recognized as a date;
345            // applied below, since detecting it only has `&self`.
346            let mut detected_num_format: Option<String> = None;
347            let (result, new_deps, compiled_to_cache) = {
348                let src = self.get_src_str_ref(&cell_ref).unwrap_or("");
349                if !src.starts_with('=') {
350                    // Numeric text is trimmed before it is parsed, because
351                    // that is what entering it does: a cell given `"  3  "`
352                    // holds the *number* 3, in Excel and (measured through
353                    // `fuzz/fuzz_vba.py`, where a macro assigned exactly that
354                    // string) through VBA's `Range.Value` as well.
355                    // `xlsx::text_cell_src` trims to match when deciding
356                    // whether an imported *text* cell needs quoting.
357                    let res = if src.is_empty() {
358                        ResultData::None
359                    } else if src.starts_with('"') && src.ends_with('"') && src.len() >= 2 {
360                        ResultData::String(src[1..src.len() - 1].to_string())
361                    } else if let Ok(i) = src.trim().parse::<i64>() {
362                        ResultData::Integer(i)
363                    } else if let Ok(f) = src.trim().parse::<f64>()
364                        // Rust's `f64::from_str` accepts "inf" and "NaN";
365                        // Excel has neither, and reports #NUM! for both. A
366                        // non-finite literal here would otherwise become a
367                        // Float that no formula could have produced -- found
368                        // by `fuzz/fuzz_vba.py`, where a macro assigned
369                        // `-2.5 ^ 1000` to a cell and this stored `-inf`
370                        // where Excel stored `#NUM!`.
371                        && f.is_finite()
372                    {
373                        ResultData::Float(f)
374                    } else if crate::core::engine::result_data::is_excel_error_code(src) {
375                        // Typing an error value into a cell produces the
376                        // error, not the text. See `is_excel_error_code`.
377                        ResultData::Error(src.to_uppercase())
378                    } else if src.eq_ignore_ascii_case("true") {
379                        ResultData::Boolean(true)
380                    } else if src.eq_ignore_ascii_case("false") {
381                        ResultData::Boolean(false)
382                    } else if let Some((date, format)) =
383                        crate::core::date::parse_date(src.trim_matches(' '))
384                    {
385                        // Excel stores a typed date as a serial and remembers
386                        // the notation as the cell's number format, so `6/22/26`
387                        // is a number that happens to display as a date.
388                        detected_num_format = Some(format.to_format_code());
389                        ResultData::Float(crate::core::date::date_to_excel_serial(date))
390                    } else if let Some(f) = crate::core::date_fn::parse_time_fraction(src) {
391                        // Same for a typed time: it is a numeric day fraction,
392                        // not text. The time number format is not represented
393                        // yet, so for now only the value semantics are matched.
394                        ResultData::Float(f)
395                    } else {
396                        ResultData::String(src.to_string())
397                    };
398                    (res, vec![], None)
399                } else {
400                    let compiled =
401                        crate::core::parser::compile_formula(src, &tables_for_compilation);
402                    let eval_src =
403                        crate::core::parser::serialize_formula(&compiled, &tables_for_compilation);
404                    let (res, deps) = match self.eval_with_row(
405                        &eval_src,
406                        context,
407                        Some(cell_ref.row),
408                        Some(cell_ref.col),
409                    ) {
410                        Ok(r) => r,
411                        Err(e) => (ResultData::Error(e.to_string()), vec![]),
412                    };
413                    let final_res = if let ResultData::None = res {
414                        ResultData::Float(0.0)
415                    } else {
416                        res
417                    };
418                    (final_res, deps, Some(compiled))
419                }
420            };
421
422            // Write compiled cache
423            if let Some(col) = self.columns.get_mut(cell_ref.col)
424                && cell_ref.row < col.compiled_src.len()
425            {
426                col.compiled_src[cell_ref.row] = compiled_to_cache.unwrap_or_default();
427            }
428
429            // Update dependencies
430            // 1. Remove old reverse dependencies
431            if let Some(old_deps) = self.dependencies_rev.remove(&cell_ref) {
432                for provider in old_deps {
433                    if let Some(dependents) = self.dependencies.get_mut(&provider) {
434                        dependents.remove(&cell_ref);
435                    }
436                }
437            }
438
439            // 2. Add new dependencies (only if not empty to save map allocations)
440            if !new_deps.is_empty() {
441                let mut new_deps_set = HashSet::new();
442                for provider in new_deps {
443                    new_deps_set.insert(provider.clone());
444                    self.dependencies
445                        .entry(provider)
446                        .or_default()
447                        .insert(cell_ref);
448                }
449                self.dependencies_rev.insert(cell_ref, new_deps_set);
450            }
451
452            // A recognized date literal carries its notation onto the cell,
453            // and a formula that shifts a date by a number inherits that
454            // date's -- `=A1+1` on a date displays as the next day, as in
455            // Excel, rather than as a bare serial.
456            let inherited = if detected_num_format.is_some()
457                || !matches!(result, ResultData::Float(_) | ResultData::Integer(_))
458            {
459                None
460            } else {
461                self.get_src_str_ref(&cell_ref)
462                    .and_then(|src| src.strip_prefix('='))
463                    .and_then(|body| crate::core::parser::parse_excel_formula(body).ok())
464                    .and_then(|ast| self.inherited_date_format(&ast))
465            };
466            // An explicit format the user (or an imported worksheet) already
467            // set wins, so re-entering a date does not clobber it.
468            if let Some(code) = detected_num_format.or(inherited) {
469                let existing = self
470                    .get_cell_style(cell_ref.row, cell_ref.col)
471                    .and_then(|s| s.num_format.clone());
472                if existing.is_none() {
473                    self.update_cell_style(cell_ref.row, cell_ref.col, |style| {
474                        style.num_format = Some(code);
475                    });
476                }
477            }
478
479            // Update data
480            if let Some(col) = self.columns.get_mut(cell_ref.col)
481                && cell_ref.row < col.data.len()
482            {
483                col.data.set(cell_ref.row, result.clone());
484                updated_cells.insert(cell_ref);
485            }
486            if let Some(comp_sheet) = tables_for_compilation
487                .iter_mut()
488                .find(|s| s.name == self.name)
489                && let Some(col) = comp_sheet.columns.get_mut(cell_ref.col)
490                && cell_ref.row < col.data.len()
491            {
492                col.data.set(cell_ref.row, result);
493            }
494
495            // Propagate to dependents (Local only)
496            // If this cell changed, we need to notify anyone who depends on THIS cell (locally).
497            // A local dependency is represented as Dependency::Local(this_cell).
498            let local_dep_key = Dependency::Local(cell_ref);
499            if let Some(dependents) = self.dependencies.get(&local_dep_key) {
500                for dependent in dependents {
501                    if !queue_set.contains(dependent) {
502                        queue.push_back(*dependent);
503                        queue_set.insert(*dependent);
504                    }
505                }
506            }
507
508            // Also notify anyone who depends on the whole COLUMN
509            let local_col_dep_key = Dependency::LocalColumn(cell_ref.col);
510            if let Some(dependents) = self.dependencies.get(&local_col_dep_key) {
511                for dependent in dependents {
512                    if !queue_set.contains(dependent) {
513                        queue.push_back(*dependent);
514                        queue_set.insert(*dependent);
515                    }
516                }
517            }
518        }
519        if initial_queue_len > 0 {
520            log::info!(
521                "Sheet '{}' commit finished. Processed {} cell updates. Total time: {:.2?}",
522                self.name,
523                ops,
524                start_commit.elapsed()
525            );
526        }
527        Ok(updated_cells)
528    }
529
530    /// Evaluates cell source text without storing it, as
531    /// [`Sheet::eval`] does, but from the point of view of `(row, col)`.
532    ///
533    /// The position is what makes relative constructs work -- a structured
534    /// reference like `[@Amount]` means "this row", so it needs to know which
535    /// row is asking. Pass `None` for both when there is no anchor.
536    ///
537    /// # Errors
538    ///
539    /// Returns an [`EngineError`] if the formula cannot be parsed. An *Excel*
540    /// error is not a Rust error: `=1/0` succeeds, returning
541    /// `ResultData::Error("#DIV/0!")`.
542    pub fn eval_with_row(
543        &self,
544        input: &str,
545        context: Option<&Context>,
546        row: Option<usize>,
547        col: Option<usize>,
548    ) -> Result<(ResultData, Vec<Dependency>), EngineError> {
549        if input.is_empty() {
550            return Ok((ResultData::None, vec![]));
551        }
552        if let Some(formula) = input.strip_prefix('=') {
553            self.eval_excel(formula, context, row, col)
554        } else {
555            if let Ok(i) = input.parse::<i64>() {
556                Ok((ResultData::Integer(i), vec![]))
557            } else if let Ok(f) = input.parse::<f64>() {
558                Ok((ResultData::Float(f), vec![]))
559            } else if let Ok(b) = input.parse::<bool>() {
560                Ok((ResultData::Boolean(b), vec![]))
561            } else {
562                Ok((ResultData::String(input.to_string()), vec![]))
563            }
564        }
565    }
566
567    /// Evaluates cell source text against this sheet without storing it,
568    /// returning the value and the references it read.
569    ///
570    /// Text with a leading `=` is a formula; anything else is parsed as a
571    /// literal. `context` supplies the other sheets, and is required for a
572    /// cross-sheet reference to resolve.
573    ///
574    /// # Errors
575    ///
576    /// Returns an [`EngineError`] if the formula cannot be parsed. An *Excel*
577    /// error is not a Rust error: `=1/0` succeeds, returning
578    /// `ResultData::Error("#DIV/0!")`.
579    pub fn eval(
580        &self,
581        input: &str,
582        context: Option<&Context>,
583    ) -> Result<(ResultData, Vec<Dependency>), EngineError> {
584        self.eval_with_row(input, context, None, None)
585    }
586
587    fn eval_excel(
588        &self,
589        code: &str,
590        context: Option<&Context>,
591        row: Option<usize>,
592        col: Option<usize>,
593    ) -> Result<(ResultData, Vec<Dependency>), EngineError> {
594        let ast = crate::core::parser::parse_excel_formula(code)
595            .map_err(|e| EngineError::EvalError(EvalError::UnknownFunction(e)))?;
596
597        let mut deps = Vec::new();
598        let result = match self.evaluate_ast(&ast, context, row, col, &mut deps, &LetScope::Empty) {
599            Ok(r) => r,
600            Err(EngineError::EvalError(EvalError::UnknownFunction(err_str)))
601                if err_str.starts_with('#') =>
602            {
603                ResultData::Error(err_str)
604            }
605            Err(e) => return Err(e),
606        };
607        Ok((result, deps))
608    }
609
610    fn evaluate_ast(
611        &self,
612        ast: &crate::core::parser::Expr,
613        context: Option<&Context>,
614        row: Option<usize>,
615        col: Option<usize>,
616        deps: &mut Vec<Dependency>,
617        scope: &LetScope<'_>,
618    ) -> Result<ResultData, EngineError> {
619        use crate::core::SheetSection;
620        use crate::core::parser::Expr;
621        use crate::core::parser::Op;
622
623        match ast {
624            Expr::Number(n) => Ok(ResultData::Float(*n)),
625            Expr::String(s) => Ok(ResultData::String(s.clone())),
626            Expr::Boolean(b) => Ok(ResultData::Boolean(*b)),
627            Expr::Error(code) => Ok(ResultData::Error(code.to_string())),
628            Expr::Identifier(name) => match scope.get(name) {
629                Some(val) => Ok(val.clone()),
630                None => Ok(ResultData::Error("#NAME?".to_string())),
631            },
632            Expr::StructuredRef {
633                sheet,
634                column,
635                is_this_row,
636                section,
637            } => {
638                let ref_name = match sheet {
639                    Some(name) => name.clone(),
640                    None => self.name.clone(),
641                };
642
643                // The leading name of a structured reference is first looked up as
644                // a real Excel Table (an `ExcelTable` may live on any sheet in
645                // scope, and is scoped to its own row/column range). If no such
646                // table exists, fall back to the legacy behavior of treating the
647                // name as a sheet name and the whole sheet as an implicit table --
648                // this keeps existing formulas working for sheets that don't
649                // define any explicit table.
650                let mut found: Option<(&Sheet, &crate::core::table::ExcelTable)> =
651                    self.find_table(&ref_name).map(|t| (self, t));
652                if found.is_none()
653                    && let Some(ctx) = context
654                {
655                    for s in ctx.sheets.values() {
656                        if let Some(t) = s.find_table(&ref_name) {
657                            found = Some((s, t));
658                            break;
659                        }
660                    }
661                }
662
663                if let Some((table_sheet, excel_table)) = found {
664                    let is_self = table_sheet.name == self.name;
665                    let sheet_name = table_sheet.name.clone();
666
667                    // (local index within the table, absolute sheet column index)
668                    let col_indices: Vec<(usize, usize)> = if let Some(col_name) = column {
669                        let local = excel_table.local_column_index(col_name).ok_or_else(|| {
670                            EngineError::EvalError(EvalError::UnknownFunction(format!(
671                                "Column not found: {}",
672                                col_name
673                            )))
674                        })?;
675                        vec![(local, excel_table.start_col + local)]
676                    } else {
677                        (0..excel_table.columns.len())
678                            .map(|local| (local, excel_table.start_col + local))
679                            .collect()
680                    };
681                    let is_whole_table = column.is_none();
682
683                    match section {
684                        SheetSection::Headers => {
685                            let names: Vec<ResultData> = col_indices
686                                .iter()
687                                .map(|&(local, _)| {
688                                    ResultData::String(
689                                        excel_table.columns.get(local).cloned().unwrap_or_default(),
690                                    )
691                                })
692                                .collect();
693                            if is_whole_table {
694                                Ok(ResultData::List(names))
695                            } else {
696                                Ok(names.into_iter().next().unwrap_or(ResultData::None))
697                            }
698                        }
699                        SheetSection::Totals => {
700                            if let Some(totals_row) = excel_table.totals_row() {
701                                let mut results = Vec::new();
702                                for &(_, col_idx) in &col_indices {
703                                    let cell_ref = CellRef::new(totals_row, col_idx);
704                                    if is_self {
705                                        deps.push(Dependency::Local(cell_ref));
706                                    } else {
707                                        deps.push(Dependency::Remote {
708                                            sheet: sheet_name.clone(),
709                                            cell: cell_ref,
710                                        });
711                                    }
712                                    results.push(table_sheet.get_result_data(&cell_ref));
713                                }
714                                if is_whole_table {
715                                    Ok(ResultData::List(results))
716                                } else {
717                                    Ok(results.into_iter().next().unwrap_or(ResultData::None))
718                                }
719                            } else {
720                                Ok(ResultData::None)
721                            }
722                        }
723                        SheetSection::Data | SheetSection::All => {
724                            if *is_this_row {
725                                let r = row.ok_or_else(|| {
726                                    EngineError::EvalError(EvalError::UnknownFunction(
727                                        "This row reference cannot be evaluated without row context"
728                                            .to_string(),
729                                    ))
730                                })?;
731                                let mut results = Vec::new();
732                                for &(_, col_idx) in &col_indices {
733                                    let cell_ref = CellRef::new(r, col_idx);
734                                    if is_self {
735                                        deps.push(Dependency::Local(cell_ref));
736                                    } else {
737                                        deps.push(Dependency::Remote {
738                                            sheet: sheet_name.clone(),
739                                            cell: cell_ref,
740                                        });
741                                    }
742                                    results.push(table_sheet.get_result_data(&cell_ref));
743                                }
744                                if is_whole_table {
745                                    Ok(ResultData::List(results))
746                                } else {
747                                    Ok(results.into_iter().next().unwrap_or(ResultData::None))
748                                }
749                            } else {
750                                // A table's column reference is bounded to
751                                // its own data rows, not the whole sheet
752                                // column -- so, like any other bounded range
753                                // (e.g. A1:A100), each cell in that range
754                                // gets its own dependency rather than a
755                                // whole-column one. Otherwise a formula
756                                // placed in the same column but *outside*
757                                // the table (a common layout, since summary
758                                // formulas often sit right below or beside
759                                // a table) would register a dependency on
760                                // its own cell and falsely trip circular-
761                                // dependency detection, which real Excel
762                                // does not do.
763                                let mut results = Vec::new();
764                                for &(_, col_idx) in &col_indices {
765                                    for r in
766                                        excel_table.data_start_row()..=excel_table.data_end_row()
767                                    {
768                                        let cell_ref = CellRef::new(r, col_idx);
769                                        if is_self {
770                                            deps.push(Dependency::Local(cell_ref));
771                                        } else {
772                                            deps.push(Dependency::Remote {
773                                                sheet: sheet_name.clone(),
774                                                cell: cell_ref,
775                                            });
776                                        }
777                                        results.push(table_sheet.get_result_data(&cell_ref));
778                                    }
779                                }
780                                Ok(ResultData::List(results))
781                            }
782                        }
783                    }
784                } else {
785                    // Legacy fallback: no explicit ExcelTable found by that name --
786                    // resolve `ref_name` as a sheet name and treat the whole sheet
787                    // as an implicit table.
788                    let sheet_name = ref_name;
789                    let is_self = sheet_name == self.name;
790
791                    let target_table = if is_self {
792                        self
793                    } else if let Some(ctx) = context {
794                        if let Some(t) = ctx.sheets.get(&sheet_name) {
795                            t
796                        } else {
797                            return Err(EngineError::EvalError(EvalError::UnknownFunction(
798                                format!("Sheet not found: {}", sheet_name),
799                            )));
800                        }
801                    } else {
802                        return Err(EngineError::EvalError(EvalError::UnknownFunction(format!(
803                            "No context to resolve sheet reference: {}",
804                            sheet_name
805                        ))));
806                    };
807
808                    // `column: None` means the reference spans every column in the
809                    // table (e.g. `Table1[#Data]` or `[@]`), rather than a single
810                    // named column.
811                    let col_indices: Vec<usize> = if let Some(col_name) = column {
812                        let pos = target_table
813                            .columns
814                            .iter()
815                            .position(|c| c.name == *col_name)
816                            .ok_or_else(|| {
817                                EngineError::EvalError(EvalError::UnknownFunction(format!(
818                                    "Column not found: {}",
819                                    col_name
820                                )))
821                            })?;
822                        vec![pos]
823                    } else {
824                        (0..target_table.columns.len()).collect()
825                    };
826                    let is_whole_table = column.is_none();
827
828                    match section {
829                        SheetSection::Headers => {
830                            let names: Vec<ResultData> = col_indices
831                                .iter()
832                                .map(|&idx| {
833                                    ResultData::String(
834                                        target_table
835                                            .columns
836                                            .get(idx)
837                                            .map(|c| c.name.clone())
838                                            .unwrap_or_default(),
839                                    )
840                                })
841                                .collect();
842                            if is_whole_table {
843                                Ok(ResultData::List(names))
844                            } else {
845                                Ok(names.into_iter().next().unwrap_or(ResultData::None))
846                            }
847                        }
848                        SheetSection::Totals => Ok(ResultData::None),
849                        SheetSection::Data | SheetSection::All => {
850                            if *is_this_row {
851                                let r = row.ok_or_else(|| {
852                                    EngineError::EvalError(EvalError::UnknownFunction(
853                                        "This row reference cannot be evaluated without row context"
854                                            .to_string(),
855                                    ))
856                                })?;
857                                let mut results = Vec::new();
858                                for &col_idx in &col_indices {
859                                    let cell_ref = CellRef::new(r, col_idx);
860                                    if is_self {
861                                        deps.push(Dependency::Local(cell_ref));
862                                    } else {
863                                        deps.push(Dependency::Remote {
864                                            sheet: sheet_name.clone(),
865                                            cell: cell_ref,
866                                        });
867                                    }
868                                    results.push(target_table.get_result_data(&cell_ref));
869                                }
870                                if is_whole_table {
871                                    Ok(ResultData::List(results))
872                                } else {
873                                    Ok(results.into_iter().next().unwrap_or(ResultData::None))
874                                }
875                            } else {
876                                let mut results = Vec::new();
877                                for &col_idx in &col_indices {
878                                    if is_self {
879                                        deps.push(Dependency::LocalColumn(col_idx));
880                                    } else {
881                                        deps.push(Dependency::RemoteColumn {
882                                            sheet: sheet_name.clone(),
883                                            col: col_idx,
884                                        });
885                                    }
886                                    for r in 0..target_table.row_count() {
887                                        let cell_ref = CellRef::new(r, col_idx);
888                                        results.push(target_table.get_result_data(&cell_ref));
889                                    }
890                                }
891                                Ok(ResultData::List(results))
892                            }
893                        }
894                    }
895                }
896            }
897            Expr::CellRef {
898                sheet,
899                row: r_val,
900                col,
901                ..
902            } => {
903                let cell_ref = CellRef::new(*r_val, *col);
904                let is_self = match sheet {
905                    Some(name) => name == &self.name,
906                    None => true,
907                };
908
909                if is_self {
910                    deps.push(Dependency::Local(cell_ref));
911                    Ok(self.get_result_data(&cell_ref))
912                } else {
913                    let name = sheet.as_ref().unwrap().clone();
914                    deps.push(Dependency::Remote {
915                        sheet: name.clone(),
916                        cell: cell_ref,
917                    });
918
919                    if let Some(ctx) = context {
920                        if let Some(t) = ctx.sheets.get(&name) {
921                            Ok(t.get_result_data(&cell_ref))
922                        } else {
923                            Err(EngineError::EvalError(EvalError::UnknownFunction(format!(
924                                "Sheet not found: {}",
925                                name
926                            ))))
927                        }
928                    } else {
929                        Err(EngineError::EvalError(EvalError::UnknownFunction(
930                            "No context to resolve sheet reference".to_string(),
931                        )))
932                    }
933                }
934            }
935            Expr::RangeRef {
936                sheet,
937                start_row,
938                start_col,
939                end_row,
940                end_col,
941                ..
942            } => {
943                let is_self = match sheet {
944                    Some(name) => name == &self.name,
945                    None => true,
946                };
947
948                let target_sheet = if is_self {
949                    Some(self)
950                } else {
951                    context.and_then(|ctx| ctx.sheets.get(sheet.as_ref().unwrap()).copied())
952                };
953
954                let actual_end_row = if *end_row == usize::MAX {
955                    target_sheet
956                        .map(|t| t.row_count().saturating_sub(1))
957                        .unwrap_or(0)
958                } else {
959                    *end_row
960                };
961                let actual_end_col = if *end_col == usize::MAX {
962                    target_sheet
963                        .map(|t| t.col_count().saturating_sub(1))
964                        .unwrap_or(0)
965                } else {
966                    *end_col
967                };
968
969                let is_col_range = *end_row == usize::MAX;
970
971                // A whole-column range's dependency is scoped to the
972                // *column*, not each individual cell, but the loop below
973                // still visits every (row, col) pair -- without tracking
974                // which columns this range has already registered, the
975                // `deps.contains` scan below (needed for correctness
976                // against whatever `deps` already held coming in) would
977                // run once per *cell* instead of once per *column*, i.e.
978                // O(width * height * deps.len()) instead of O(width *
979                // deps.len()). For a wide range (e.g. `=C:LL`, 322
980                // columns) evaluated repeatedly (e.g. inside a self-
981                // referential formula bounded by commit()'s max_ops, see
982                // the fix just above), that quadratic-in-width blowup was
983                // the difference between finishing in under a second and
984                // taking tens of seconds to minutes -- found via the same
985                // visi-core/fuzz formula_eval run (#26).
986                let mut seen_col_deps: HashSet<usize> = HashSet::new();
987
988                let mut results = Vec::new();
989                for r in *start_row..=actual_end_row {
990                    for c in *start_col..=actual_end_col {
991                        let cell_ref = CellRef::new(r, c);
992                        if is_self {
993                            if is_col_range {
994                                if seen_col_deps.insert(c) {
995                                    let col_dep = Dependency::LocalColumn(c);
996                                    if !deps.contains(&col_dep) {
997                                        deps.push(col_dep);
998                                    }
999                                }
1000                            } else {
1001                                deps.push(Dependency::Local(cell_ref));
1002                            }
1003                            // A range that includes the very cell this
1004                            // formula lives in (most commonly a bare,
1005                            // unaggregated whole-column/whole-row range
1006                            // like `=C:P` sitting inside columns C..P)
1007                            // must not read that cell's own currently
1008                            // stored value back into itself: on every
1009                            // recompute the stored value *is* this List,
1010                            // so reading it back would nest a List inside
1011                            // itself one level deeper each pass --
1012                            // unbounded growth that only stops at a stack
1013                            // overflow in recursive Clone/Drop, found via
1014                            // visi-core/fuzz's formula_eval target (#26).
1015                            // Blank matches this engine's existing
1016                            // convention for an unresolvable self-read
1017                            // elsewhere (e.g. ISBLANK(GET(...)) on an
1018                            // empty cell).
1019                            if row == Some(r) && col == Some(c) {
1020                                results.push(ResultData::None);
1021                            } else {
1022                                results.push(self.get_result_data(&cell_ref));
1023                            }
1024                        } else {
1025                            let name = sheet.as_ref().unwrap().clone();
1026                            if is_col_range {
1027                                if seen_col_deps.insert(c) {
1028                                    let col_dep = Dependency::RemoteColumn {
1029                                        sheet: name.clone(),
1030                                        col: c,
1031                                    };
1032                                    if !deps.contains(&col_dep) {
1033                                        deps.push(col_dep);
1034                                    }
1035                                }
1036                            } else {
1037                                deps.push(Dependency::Remote {
1038                                    sheet: name.clone(),
1039                                    cell: cell_ref,
1040                                });
1041                            }
1042                            if let Some(ctx) = context {
1043                                if let Some(t) = ctx.sheets.get(&name) {
1044                                    results.push(t.get_result_data(&cell_ref));
1045                                } else {
1046                                    return Err(EngineError::EvalError(
1047                                        EvalError::UnknownFunction(format!(
1048                                            "Sheet not found: {}",
1049                                            name
1050                                        )),
1051                                    ));
1052                                }
1053                            } else {
1054                                return Err(EngineError::EvalError(EvalError::UnknownFunction(
1055                                    "No context to resolve sheet reference".to_string(),
1056                                )));
1057                            }
1058                        }
1059                    }
1060                }
1061                Ok(ResultData::List(results))
1062            }
1063            Expr::List(list) => {
1064                let mut results = Vec::new();
1065                for item in list {
1066                    results.push(self.evaluate_ast(item, context, row, col, deps, scope)?);
1067                }
1068                Ok(ResultData::List(results))
1069            }
1070            Expr::UnaryOp { op, expr } => {
1071                let val = self.evaluate_ast(expr, context, row, col, deps, scope)?;
1072                match op {
1073                    Op::Sub => match val {
1074                        ResultData::Float(f) => Ok(ResultData::Float(-f)),
1075                        ResultData::Integer(i) => Ok(ResultData::Integer(-i)),
1076                        _ => Err(EngineError::EvalError(EvalError::UnknownFunction(
1077                            "Unary minus expects number".to_string(),
1078                        ))),
1079                    },
1080                    _ => Ok(val),
1081                }
1082            }
1083            Expr::BinaryOp { op, left, right } => {
1084                let l_val = self.evaluate_ast(left, context, row, col, deps, scope)?;
1085
1086                match op {
1087                    Op::Eq | Op::Ne | Op::Lt | Op::Gt | Op::Le | Op::Ge => {
1088                        if let ResultData::Error(_) = &l_val {
1089                            return Ok(l_val);
1090                        }
1091                        let r_val = self.evaluate_ast(right, context, row, col, deps, scope)?;
1092                        if let ResultData::Error(_) = &r_val {
1093                            return Ok(r_val);
1094                        }
1095                        let ord = Self::compare_excel_values(&l_val, &r_val);
1096                        let b = match op {
1097                            Op::Eq => ord.is_eq(),
1098                            Op::Ne => !ord.is_eq(),
1099                            Op::Lt => ord.is_lt(),
1100                            Op::Gt => ord.is_gt(),
1101                            Op::Le => ord.is_le(),
1102                            Op::Ge => ord.is_ge(),
1103                            _ => unreachable!(),
1104                        };
1105                        Ok(ResultData::Boolean(b))
1106                    }
1107                    _ => {
1108                        if let ResultData::Error(_) = &l_val {
1109                            return Ok(l_val);
1110                        }
1111                        let lf = match self.to_f64(&l_val) {
1112                            Some(f) => f,
1113                            None => return Ok(ResultData::Error("#VALUE!".to_string())),
1114                        };
1115                        let r_val = self.evaluate_ast(right, context, row, col, deps, scope)?;
1116                        if let ResultData::Error(_) = &r_val {
1117                            return Ok(r_val);
1118                        }
1119                        let rf = match self.to_f64(&r_val) {
1120                            Some(f) => f,
1121                            None => return Ok(ResultData::Error("#VALUE!".to_string())),
1122                        };
1123                        match op {
1124                            Op::Add => Ok(ResultData::Float(lf + rf)),
1125                            Op::Sub => Ok(ResultData::Float(lf - rf)),
1126                            Op::Mul => Ok(ResultData::Float(lf * rf)),
1127                            Op::Div => {
1128                                if rf == 0.0 {
1129                                    return Ok(ResultData::Error("#DIV/0!".to_string()));
1130                                }
1131                                Ok(ResultData::Float(lf / rf))
1132                            }
1133                            Op::Exp => {
1134                                if lf == 0.0 && rf == 0.0 {
1135                                    return Ok(ResultData::Error("#NUM!".to_string()));
1136                                }
1137                                if lf == 0.0 && rf < 0.0 {
1138                                    return Ok(ResultData::Error("#DIV/0!".to_string()));
1139                                }
1140                                if lf < 0.0 {
1141                                    if rf.fract() != 0.0 || rf.abs() > 1e6 {
1142                                        return Ok(ResultData::Error("#NUM!".to_string()));
1143                                    }
1144                                    let res = lf.powi(rf as i32);
1145                                    if res.is_nan() || res.is_infinite() {
1146                                        return Ok(ResultData::Error("#NUM!".to_string()));
1147                                    }
1148                                    return Ok(ResultData::Float(res));
1149                                }
1150                                let res = lf.powf(rf);
1151                                if res.is_nan() || res.is_infinite() {
1152                                    return Ok(ResultData::Error("#NUM!".to_string()));
1153                                }
1154                                Ok(ResultData::Float(res))
1155                            }
1156                            _ => unreachable!(),
1157                        }
1158                    }
1159                }
1160            }
1161            Expr::FunctionCall { name, args } => {
1162                self.evaluate_function(name, args, context, row, col, deps, scope)
1163            }
1164        }
1165    }
1166
1167    fn excel_type_rank(val: &ResultData) -> u8 {
1168        match val {
1169            ResultData::None => 0,
1170            ResultData::Integer(_) | ResultData::Float(_) => 1,
1171            ResultData::String(_) => 2,
1172            ResultData::Boolean(_) => 3,
1173            _ => 4,
1174        }
1175    }
1176
1177    fn compare_excel_values(l: &ResultData, r: &ResultData) -> std::cmp::Ordering {
1178        // Coerce ResultData::None against the type of the opposing operand
1179        match (l, r) {
1180            (ResultData::None, ResultData::None) => return std::cmp::Ordering::Equal,
1181            (ResultData::None, ResultData::Integer(b)) => {
1182                return 0.0
1183                    .partial_cmp(&(*b as f64))
1184                    .unwrap_or(std::cmp::Ordering::Equal);
1185            }
1186            (ResultData::None, ResultData::Float(b)) => {
1187                return 0.0.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
1188            }
1189            (ResultData::Integer(a), ResultData::None) => {
1190                return (*a as f64)
1191                    .partial_cmp(&0.0)
1192                    .unwrap_or(std::cmp::Ordering::Equal);
1193            }
1194            (ResultData::Float(a), ResultData::None) => {
1195                return a.partial_cmp(&0.0).unwrap_or(std::cmp::Ordering::Equal);
1196            }
1197            (ResultData::None, ResultData::String(b)) => {
1198                return "".cmp(b.to_lowercase().as_str());
1199            }
1200            (ResultData::String(a), ResultData::None) => {
1201                return a.to_lowercase().as_str().cmp("");
1202            }
1203            (ResultData::None, ResultData::Boolean(b)) => {
1204                return false.cmp(b);
1205            }
1206            (ResultData::Boolean(a), ResultData::None) => {
1207                return a.cmp(&false);
1208            }
1209            _ => {}
1210        }
1211
1212        let rank_l = Self::excel_type_rank(l);
1213        let rank_r = Self::excel_type_rank(r);
1214        if rank_l != rank_r {
1215            return rank_l.cmp(&rank_r);
1216        }
1217        match (l, r) {
1218            (ResultData::Integer(a), ResultData::Integer(b)) => a.cmp(b),
1219            (ResultData::Float(a), ResultData::Float(b)) => {
1220                a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
1221            }
1222            (ResultData::Integer(a), ResultData::Float(b)) => (*a as f64)
1223                .partial_cmp(b)
1224                .unwrap_or(std::cmp::Ordering::Equal),
1225            (ResultData::Float(a), ResultData::Integer(b)) => a
1226                .partial_cmp(&(*b as f64))
1227                .unwrap_or(std::cmp::Ordering::Equal),
1228            (ResultData::Boolean(a), ResultData::Boolean(b)) => a.cmp(b),
1229            (ResultData::String(a), ResultData::String(b)) => Self::compare_excel_strings(a, b),
1230            _ => std::cmp::Ordering::Equal,
1231        }
1232    }
1233
1234    /// `SORT`/`SORTBY`-specific comparator: Microsoft documents that both
1235    /// functions always place blank cells last, regardless of ascending
1236    /// vs. descending order -- unlike `compare_excel_values`'s general
1237    /// blank-coerces-to-0/""/false rule (correct for comparison operators,
1238    /// MATCH, etc.), which would otherwise rank a blank ahead of every
1239    /// negative number once descending order reverses the comparison.
1240    /// Found via the differential fuzzer: `SORT({-215.8,,-100,-240.97,-88},1,-1)`
1241    /// put the blank first (coerced to 0, the largest value once reversed)
1242    /// instead of last, so `INDEX(...,1)` returned 0 instead of -88.
1243    fn sort_compare_blanks_last(
1244        l: &ResultData,
1245        r: &ResultData,
1246        sort_order: f64,
1247    ) -> std::cmp::Ordering {
1248        match (matches!(l, ResultData::None), matches!(r, ResultData::None)) {
1249            (true, true) => std::cmp::Ordering::Equal,
1250            (true, false) => std::cmp::Ordering::Greater,
1251            (false, true) => std::cmp::Ordering::Less,
1252            (false, false) => {
1253                let ord = Self::compare_excel_values(l, r);
1254                if sort_order < 0.0 { ord.reverse() } else { ord }
1255            }
1256        }
1257    }
1258
1259    fn is_excel_number_str(s: &str) -> bool {
1260        let s = s.trim();
1261        if s.is_empty() {
1262            return false;
1263        }
1264        let bytes = s.as_bytes();
1265        let first = bytes[0];
1266        if first == b'e' || first == b'E' {
1267            return false;
1268        }
1269        if (first == b'+' || first == b'-') && bytes.len() > 1 {
1270            let second = bytes[1];
1271            if second == b'e' || second == b'E' {
1272                return false;
1273            }
1274        }
1275        true
1276    }
1277
1278    /// Excel's `>`/`<` text comparison ignores `-` entirely as long as it
1279    /// isn't the only difference between the two strings, and only once
1280    /// the hyphen-stripped strings are otherwise identical does the
1281    /// hyphen's presence break the tie -- in which case the string that
1282    /// *has* the hyphen sorts greater. Measured directly (win32com,
1283    /// real Windows Excel), since none of this is documented and a
1284    /// zipped per-position weighting (this function's previous
1285    /// implementation, which gave `-` a low weight so it sorted before
1286    /// any digit) gets it backwards:
1287    ///   `"-4463" > "36-33"` is TRUE (decided by the hyphen-stripped
1288    ///   digits, `4463` vs `3633`; a low weight for `-` would make
1289    ///   `"-4463"` sort first instead, e.g. `visi vs Excel` on
1290    ///   `IF(CONCATENATE(-44,J5) > CONCATENATE(36,J3), ...)`,
1291    ///   fuzz/fuzz_excel.py seed 707537)
1292    ///   `"a-1" > "a2"` is FALSE (stripped `"a1"` vs `"a2"`)
1293    ///   `"-1" > "-2"` is FALSE, `"-2" > "-1"` is TRUE (both sides'
1294    ///   hyphens strip away, leaving a plain digit compare)
1295    ///   `"-a" > "a"` is TRUE, `"1-2" > "12"` is TRUE (stripped content
1296    ///   ties, so the longer, hyphen-bearing original wins the tie-break)
1297    /// `(`/`)` keep their existing low weight, unaffected by this.
1298    fn compare_excel_strings(a: &str, b: &str) -> std::cmp::Ordering {
1299        let a_low = a.to_lowercase();
1300        let b_low = b.to_lowercase();
1301        let strip_hyphens = |s: &str| -> String { s.chars().filter(|&c| c != '-').collect() };
1302        let a_stripped = strip_hyphens(&a_low);
1303        let b_stripped = strip_hyphens(&b_low);
1304        let char_weight = |ch: char| -> u32 {
1305            match ch {
1306                '(' => 2,
1307                ')' => 3,
1308                _ => (ch as u32) + 10,
1309            }
1310        };
1311        for (ca, cb) in a_stripped.chars().zip(b_stripped.chars()) {
1312            if ca != cb {
1313                return char_weight(ca).cmp(&char_weight(cb));
1314            }
1315        }
1316        match a_stripped.len().cmp(&b_stripped.len()) {
1317            std::cmp::Ordering::Equal => a_low.len().cmp(&b_low.len()),
1318            other => other,
1319        }
1320    }
1321
1322    /// Snaps a float to its 15-significant-digit rounding when the two are
1323    /// within floating-point noise of each other, so accumulated error does
1324    /// not leak into a result Excel would show as exact.
1325    ///
1326    /// Left alone if the rounding moves the value by more than that, and for
1327    /// zero and non-finite values.
1328    pub(crate) fn clean_float(val: f64) -> f64 {
1329        if val == 0.0 || !val.is_finite() {
1330            return val;
1331        }
1332        let abs_val = val.abs();
1333        let exp = abs_val.log10().floor() as i32;
1334        let factor = 10.0f64.powi(15 - 1 - exp);
1335        if factor.is_finite() && factor != 0.0 {
1336            let rounded = (val * factor).round() / factor;
1337            if (val - rounded).abs() <= 1e-14 * abs_val {
1338                return rounded;
1339            }
1340        }
1341        val
1342    }
1343
1344    /// Coerces a value to a number the way an Excel arithmetic operator does:
1345    /// a blank is 0, a boolean is 0 or 1, and text is converted if it reads as
1346    /// a number or a date (a date becoming its serial).
1347    ///
1348    /// `None` for text that is not numeric and for every other value,
1349    /// including errors -- callers turn that into `#VALUE!`.
1350    ///
1351    /// Not every function coerces this way; the stricter families reject text
1352    /// and booleans outright.
1353    pub(crate) fn to_f64(&self, val: &ResultData) -> Option<f64> {
1354        match val {
1355            ResultData::None => Some(0.0),
1356            ResultData::Float(f) => Some(*f),
1357            ResultData::Integer(i) => Some(*i as f64),
1358            ResultData::Boolean(b) => Some(if *b { 1.0 } else { 0.0 }),
1359            ResultData::String(s) => {
1360                let s_trim = s.trim();
1361                if Self::is_excel_number_str(s_trim) {
1362                    if let Ok(f) = s_trim.parse::<f64>() {
1363                        return Some(f);
1364                    }
1365                    if let Some((date, _)) = crate::core::date::parse_date(s_trim) {
1366                        return Some(crate::core::date::date_to_excel_serial(date));
1367                    }
1368                    None
1369                } else if let Some((date, _)) = crate::core::date::parse_date(s_trim) {
1370                    Some(crate::core::date::date_to_excel_serial(date))
1371                } else {
1372                    None
1373                }
1374            }
1375            _ => None,
1376        }
1377    }
1378
1379    fn to_f64_arg(&self, arg_opt: Option<&ResultData>, fn_name: &str) -> Result<f64, EngineError> {
1380        let val = arg_opt.ok_or_else(|| {
1381            EngineError::EvalError(EvalError::UnknownFunction(format!(
1382                "{} requires argument",
1383                fn_name
1384            )))
1385        })?;
1386        if let ResultData::Error(e) = val {
1387            return Err(EngineError::EvalError(EvalError::UnknownFunction(
1388                e.clone(),
1389            )));
1390        }
1391        self.to_f64(val).ok_or_else(|| {
1392            EngineError::EvalError(EvalError::UnknownFunction("#VALUE!".to_string()))
1393        })
1394    }
1395
1396    fn find_error_in_args(args: &[ResultData]) -> Option<ResultData> {
1397        for arg in args {
1398            match arg {
1399                ResultData::Error(_) => return Some(arg.clone()),
1400                ResultData::List(list) => {
1401                    if let Some(err) = Self::find_error_in_args(list) {
1402                        return Some(err);
1403                    }
1404                }
1405                _ => {}
1406            }
1407        }
1408        None
1409    }
1410
1411    fn check_arg_errors(&self, args: &[ResultData], is_direct: &[bool]) -> Option<ResultData> {
1412        for (i, arg) in args.iter().enumerate() {
1413            match arg {
1414                ResultData::Error(_) => return Some(arg.clone()),
1415                ResultData::List(list) => {
1416                    if let Some(err) = self.check_arg_errors(list, &[]) {
1417                        return Some(err);
1418                    }
1419                }
1420                ResultData::String(_)
1421                    if is_direct.get(i).copied().unwrap_or(false) && self.to_f64(arg).is_none() =>
1422                {
1423                    return Some(ResultData::Error("#VALUE!".to_string()));
1424                }
1425                _ => {}
1426            }
1427        }
1428        None
1429    }
1430
1431    fn sum_helper(&self, arg: &ResultData, is_direct: bool) -> f64 {
1432        match arg {
1433            ResultData::Float(f) => *f,
1434            ResultData::Integer(i) => *i as f64,
1435            ResultData::Boolean(b) => {
1436                if is_direct {
1437                    if *b { 1.0 } else { 0.0 }
1438                } else {
1439                    0.0
1440                }
1441            }
1442            ResultData::String(_) => {
1443                if is_direct {
1444                    self.to_f64(arg).unwrap_or(0.0)
1445                } else {
1446                    0.0
1447                }
1448            }
1449            ResultData::List(list) => {
1450                let mut sum = 0.0;
1451                for item in list {
1452                    sum += self.sum_helper(item, false);
1453                }
1454                sum
1455            }
1456            _ => 0.0,
1457        }
1458    }
1459
1460    /// Flattens a single argument (which may be a range/array `List`) into
1461    /// an ordered `Vec<f64>` for the financial functions that take a
1462    /// cashflow series (`NPV`, `IRR`, `MIRR`, `XNPV`, `XIRR`, `FVSCHEDULE`).
1463    /// Mirrors `sum_helper`'s convention: booleans/text only count when
1464    /// passed directly (not through a range).
1465    fn flatten_finance_numbers(&self, arg: &ResultData, is_direct: bool) -> Vec<f64> {
1466        match arg {
1467            ResultData::Float(f) => vec![*f],
1468            ResultData::Integer(i) => vec![*i as f64],
1469            ResultData::Boolean(b) => {
1470                if is_direct {
1471                    vec![if *b { 1.0 } else { 0.0 }]
1472                } else {
1473                    vec![]
1474                }
1475            }
1476            ResultData::String(_) => {
1477                if is_direct {
1478                    self.to_f64(arg).into_iter().collect()
1479                } else {
1480                    vec![]
1481                }
1482            }
1483            ResultData::List(list) => list
1484                .iter()
1485                .flat_map(|v| self.flatten_finance_numbers(v, false))
1486                .collect(),
1487            _ => vec![],
1488        }
1489    }
1490
1491    fn flatten_stat_numbers(&self, arg: &ResultData, is_direct: bool) -> Vec<f64> {
1492        match arg {
1493            ResultData::Float(f) => vec![*f],
1494            ResultData::Integer(i) => vec![*i as f64],
1495            ResultData::Boolean(b) => {
1496                if is_direct {
1497                    vec![if *b { 1.0 } else { 0.0 }]
1498                } else {
1499                    vec![]
1500                }
1501            }
1502            ResultData::String(_) => {
1503                if is_direct {
1504                    self.to_f64(arg).into_iter().collect()
1505                } else {
1506                    vec![]
1507                }
1508            }
1509            ResultData::List(list) => list
1510                .iter()
1511                .flat_map(|v| self.flatten_stat_numbers(v, false))
1512                .collect(),
1513            _ => vec![],
1514        }
1515    }
1516
1517    /// Flattens one argument positionally: `Some(n)` for a numeric cell,
1518    /// `None` for anything real Excel excludes from a paired statistical
1519    /// calculation (text, boolean, blank). Unlike flatten_stat_numbers,
1520    /// excluded cells still occupy a slot, so two ranges of the same
1521    /// shape always produce vectors of the same length and element `i` of
1522    /// one still lines up with element `i` of the other.
1523    fn flatten_positional(
1524        &self,
1525        arg: &ResultData,
1526        out: &mut Vec<Option<f64>>,
1527        first_err: &mut Option<String>,
1528    ) {
1529        match arg {
1530            ResultData::List(items) => {
1531                for item in items {
1532                    self.flatten_positional(item, out, first_err);
1533                }
1534            }
1535            ResultData::Float(f) => out.push(Some(*f)),
1536            ResultData::Integer(i) => out.push(Some(*i as f64)),
1537            ResultData::Error(e) => {
1538                if first_err.is_none() {
1539                    *first_err = Some(e.clone());
1540                }
1541                out.push(None);
1542            }
1543            _ => out.push(None),
1544        }
1545    }
1546
1547    fn positional_numbers(
1548        &self,
1549        arg: Option<&ResultData>,
1550        first_err: &mut Option<String>,
1551    ) -> Vec<Option<f64>> {
1552        let mut out = Vec::new();
1553        if let Some(a) = arg {
1554            self.flatten_positional(a, &mut out, first_err);
1555        }
1556        out
1557    }
1558
1559    /// Excel's paired statistical functions (CORREL/PEARSON/COVAR/
1560    /// COVARIANCE.P/COVARIANCE.S/SLOPE/INTERCEPT/RSQ/STEYX/FORECAST/
1561    /// TREND/LINEST/GROWTH/LOGEST/T.TEST/SUMX2PY2/SUMXMY2/SUMX2MY2/PROB)
1562    /// compare the two ranges' *raw* element counts first -- a mismatch
1563    /// is #N/A regardless of content -- and then drop every (x, y) pair
1564    /// where either side is non-numeric, keeping what survives aligned.
1565    ///
1566    /// Verified directly against real Excel: `COVAR(A1:A4, B1:B4)` with
1567    /// one text cell in B returns exactly the value of the 3-element
1568    /// ranges with that whole pair physically removed, and the same holds
1569    /// for SLOPE/INTERCEPT/RSQ/PEARSON/STEYX/FORECAST/T.TEST/SUMX*.
1570    /// Booleans and blanks are excluded the same way text is.
1571    ///
1572    /// This is deliberately *not* the same as flattening each side
1573    /// independently (what flatten_stat_numbers does): dropping a
1574    /// non-numeric from only one side shifts every later element against
1575    /// its partner, silently correlating the wrong values together.
1576    /// F.TEST/FTEST is the exception that genuinely does want independent
1577    /// per-array flattening -- it compares two samples' variances and
1578    /// doesn't require equal sizes at all (confirmed against real Excel:
1579    /// `FTEST(4-cell-with-text, ...)` equals `FTEST(full-4-cell, ...)`
1580    /// against the 3-cell survivor, i.e. each side shrinks on its own).
1581    fn pair_and_filter(
1582        xs_raw: Vec<Option<f64>>,
1583        ys_raw: Vec<Option<f64>>,
1584    ) -> Result<(Vec<f64>, Vec<f64>), String> {
1585        if xs_raw.len() != ys_raw.len() {
1586            return Err("#N/A".to_string());
1587        }
1588        let mut xs = Vec::with_capacity(xs_raw.len());
1589        let mut ys = Vec::with_capacity(ys_raw.len());
1590        for (x, y) in xs_raw.into_iter().zip(ys_raw) {
1591            if let (Some(x), Some(y)) = (x, y) {
1592                xs.push(x);
1593                ys.push(y);
1594            }
1595        }
1596        Ok((xs, ys))
1597    }
1598
1599    /// pair_and_filter over two argument slots.
1600    fn paired_args(
1601        &self,
1602        x_arg: Option<&ResultData>,
1603        y_arg: Option<&ResultData>,
1604    ) -> Result<(Vec<f64>, Vec<f64>), String> {
1605        // The size check has to come *before* propagating any error cell
1606        // sitting inside either range: real Excel reports #N/A for two
1607        // differently-sized ranges even when one of them contains a live
1608        // error (confirmed by probing `CORREL` over a 4-cell and a 3-cell
1609        // range whose second range held a #DIV/0!, which answers #N/A).
1610        // These functions are therefore excluded from the generic
1611        // "any error in an argument short-circuits the call" pre-pass, and
1612        // re-raise the error here only once the shapes agree.
1613        // A *scalar* operand carrying an error propagates before any
1614        // shape logic runs: Excel resolves a 1x1 reference to a plain
1615        // value first, and an error value in an ordinary operand position
1616        // short-circuits the call. So `SUMX2PY2(A1:A4, P1:P1)` with a
1617        // #DIV/0! in P1 is #DIV/0!, even though the two operands are
1618        // differently sized.
1619        //
1620        // An error inside a *multi-cell* range does not get that
1621        // treatment -- there the size check wins, and
1622        // `SUMX2PY2(A1:A4, N1:N3)` with an error inside N1:N3 is #N/A.
1623        // Both confirmed against real Excel, and consistently across
1624        // CORREL/SLOPE/STEYX/SUMX2PY2.
1625        for arg in [x_arg, y_arg].into_iter().flatten() {
1626            // A one-cell *range* evaluates to a one-element List rather
1627            // than a bare scalar, so both spellings have to be unwrapped
1628            // here -- matching only the bare form let
1629            // `STEYX(H6:H6, F2:H2)` report the shape mismatch (#N/A)
1630            // instead of the error sitting in H6.
1631            let scalar = match arg {
1632                ResultData::List(items) if items.len() == 1 => &items[0],
1633                other => other,
1634            };
1635            if let ResultData::Error(e) = scalar {
1636                return Err(e.clone());
1637            }
1638            // A one-cell operand that is *empty* isn't a one-element array,
1639            // it's a missing operand: Excel answers #VALUE! rather than the
1640            // #N/A a shape mismatch would give. Note this is specifically
1641            // about blankness -- a one-cell operand holding text or a
1642            // boolean still reports #N/A, so it can't be folded into the
1643            // general non-numeric handling (all three confirmed against
1644            // real Excel with CORREL against a 4-cell range).
1645            if Self::is_empty_scalar_operand(arg) {
1646                return Err("#VALUE!".to_string());
1647            }
1648        }
1649        let mut first_err = None;
1650        let xs_raw = self.positional_numbers(x_arg, &mut first_err);
1651        let ys_raw = self.positional_numbers(y_arg, &mut first_err);
1652        if xs_raw.len() != ys_raw.len() {
1653            return Err("#N/A".to_string());
1654        }
1655        if let Some(e) = first_err {
1656            return Err(e);
1657        }
1658        Self::pair_and_filter(xs_raw, ys_raw)
1659    }
1660
1661    /// Like flatten_stat_numbers, but errors instead of silently dropping
1662    /// a cell real Excel won't accept. Excel's array/matrix-argument
1663    /// functions don't ignore text the way SUM/AVERAGE-style aggregates
1664    /// do -- one bad cell makes the whole call #VALUE!.
1665    ///
1666    /// `blanks` selects between the three blank-handling behaviours real
1667    /// Excel actually exhibits here, each established by probing it
1668    /// directly:
1669    ///  - `BlankPolicy::Zero` (MULTINOMIAL): a blank counts as 0 and the
1670    ///    call still succeeds -- `MULTINOMIAL(3, <blank>)` is 1, the
1671    ///    blank participating as a zero.
1672    ///  - `BlankPolicy::Skip` (GCD/LCM/SERIESSUM): a blank is dropped
1673    ///    outright rather than zero-filled. `LCM(1, <blank>)` is 1 (as if
1674    ///    `LCM(1)`), not `LCM(1, 0)` = 0. For SERIESSUM this also shifts
1675    ///    every later coefficient down a power:
1676    ///    `SERIESSUM(0.5, 0, 2, {4, 6, <blank>, 8})` is 6.0 -- exactly the
1677    ///    3-coefficient answer -- not the 5.625 a zero in that slot gives.
1678    ///  - `BlankPolicy::Reject` (LINEST/TREND/GROWTH/LOGEST/MMULT): blanks
1679    ///    are #VALUE! too, same as text and booleans.
1680    ///
1681    /// `coerce_text` selects separately whether a numeric-looking string
1682    /// is accepted (converted the same way `to_f64` would) or rejected
1683    /// outright as #VALUE! -- this does *not* track the blank policy,
1684    /// since GCD/LCM (`Skip`) coerce text (`GCD("12", 8)` = 4) while
1685    /// SERIESSUM (also `Skip`) does not (`SERIESSUM(1.49, 1, 2,
1686    /// {<blank>, "2", 27, -35})` is #VALUE! in real Excel, not the number
1687    /// the coerced "2" would give -- fuzz/fuzz_excel.py seed 107768).
1688    /// Booleans are always rejected regardless of either policy -- `GCD(TRUE,
1689    /// 8)` is #VALUE! -- which is why this can't just fall through to
1690    /// `to_f64`, the lenient coercion used for scalar arguments.
1691    fn flatten_strict_inner(
1692        &self,
1693        arg: &ResultData,
1694        blanks: BlankPolicy,
1695        coerce_text: bool,
1696        out: &mut Vec<f64>,
1697    ) -> Result<(), String> {
1698        match arg {
1699            ResultData::List(items) => {
1700                for item in items {
1701                    self.flatten_strict_inner(item, blanks, coerce_text, out)?;
1702                }
1703                Ok(())
1704            }
1705            ResultData::Error(e) => Err(e.clone()),
1706            ResultData::Float(f) => {
1707                out.push(*f);
1708                Ok(())
1709            }
1710            ResultData::Integer(i) => {
1711                out.push(*i as f64);
1712                Ok(())
1713            }
1714            ResultData::None => match blanks {
1715                BlankPolicy::Zero => {
1716                    out.push(0.0);
1717                    Ok(())
1718                }
1719                BlankPolicy::Skip => Ok(()),
1720                BlankPolicy::Reject => Err("#VALUE!".to_string()),
1721            },
1722            // Numeric text is coerced, non-numeric text is not, when
1723            // `coerce_text` is set: real Excel gives GCD("12", 8) = 4,
1724            // LCM("4", 6) = 12 and MULTINOMIAL("3", 2) = 10, while
1725            // GCD("x", 8) is #VALUE! either way. Booleans stay rejected
1726            // regardless -- GCD(TRUE, 8) is #VALUE! -- which is why this
1727            // can't just fall through to `to_f64`.
1728            ResultData::String(_) if coerce_text => match self.to_f64(arg) {
1729                Some(f) => {
1730                    out.push(f);
1731                    Ok(())
1732                }
1733                None => Err("#VALUE!".to_string()),
1734            },
1735            _ => Err("#VALUE!".to_string()),
1736        }
1737    }
1738
1739    fn flatten_strict_numbers(&self, arg: &ResultData) -> Result<Vec<f64>, String> {
1740        let mut out = Vec::new();
1741        self.flatten_strict_inner(arg, BlankPolicy::Zero, true, &mut out)?;
1742        Ok(out)
1743    }
1744
1745    /// flatten_strict_numbers with blanks dropped rather than zero-filled,
1746    /// for GCD/LCM (which also coerce numeric text, like MULTINOMIAL).
1747    fn flatten_skipping_blanks(&self, arg: Option<&ResultData>) -> Result<Vec<f64>, String> {
1748        let mut out = Vec::new();
1749        if let Some(a) = arg {
1750            self.flatten_strict_inner(a, BlankPolicy::Skip, true, &mut out)?;
1751        }
1752        Ok(out)
1753    }
1754
1755    /// Like `flatten_skipping_blanks`, but a numeric-looking string is
1756    /// #VALUE! rather than coerced -- SERIESSUM's coefficients, unlike
1757    /// GCD/LCM's operands, don't accept text at all (measured:
1758    /// `SERIESSUM(1.49, 1, 2, {<blank>, "2", 27, -35})` is #VALUE! in real
1759    /// Excel, not the value the coerced "2" would give -- see
1760    /// `flatten_strict_inner`'s doc comment).
1761    fn flatten_skipping_blanks_no_text_coercion(
1762        &self,
1763        arg: Option<&ResultData>,
1764    ) -> Result<Vec<f64>, String> {
1765        let mut out = Vec::new();
1766        if let Some(a) = arg {
1767            self.flatten_strict_inner(a, BlankPolicy::Skip, false, &mut out)?;
1768        }
1769        Ok(out)
1770    }
1771
1772    /// flatten_strict_numbers with the stricter "a blank is also #VALUE!"
1773    /// rule the regression-array and matrix functions use.
1774    fn flatten_numbers_only(&self, arg: &ResultData) -> Result<Vec<f64>, String> {
1775        let mut out = Vec::new();
1776        self.flatten_strict_inner(arg, BlankPolicy::Reject, false, &mut out)?;
1777        Ok(out)
1778    }
1779
1780    /// The value of one cell of a SUMIF/AVERAGEIF/MAXIFS/MINIFS-style
1781    /// *aggregate* range. Only a real number counts: Excel silently skips
1782    /// text and booleans in the range being summed/averaged/compared
1783    /// (confirmed directly -- `SUMIF` over a range holding
1784    /// `{100, TRUE, 200, "txt", 300}` is 600, and MAXIFS over the same
1785    /// range is 300, not the boolean coerced to 1). Using the lenient
1786    /// `to_f64` here instead folded `TRUE` in as a 1, which both shifted
1787    /// sums/averages and could win a MAX/MIN outright.
1788    fn aggregate_range_number(val: &ResultData) -> Option<f64> {
1789        match val {
1790            ResultData::Float(f) => Some(*f),
1791            ResultData::Integer(i) => Some(*i as f64),
1792            _ => None,
1793        }
1794    }
1795
1796    fn flatten_numbers_only_arg(&self, arg: Option<&ResultData>) -> Result<Vec<f64>, String> {
1797        match arg {
1798            Some(a) => self.flatten_numbers_only(a),
1799            None => Ok(vec![]),
1800        }
1801    }
1802
1803    /// `flatten_stat_numbers` across an argument list, applying Excel's rule
1804    /// for text supplied *directly* as an argument: it is coerced if it
1805    /// looks numeric, and is `#VALUE!` if it does not. Text reached through
1806    /// a reference is skipped instead, which is what `flatten_stat_numbers`
1807    /// already does on its own.
1808    ///
1809    /// The split matters because silently skipping uncoercible direct text
1810    /// turns a wrong formula into a plausible number: `DEVSQ("abc",3,4,5)`
1811    /// answered 2 (the spread of the remaining three) where Excel answers
1812    /// `#VALUE!`. Verified against real Excel for SUM, AVERAGE, DEVSQ,
1813    /// STDEV, VAR, MEDIAN, MAX, MIN, PRODUCT, SUMSQ, GEOMEAN, AVEDEV, SKEW
1814    /// and KURT. COUNT is the deliberate exception -- it never errors, it
1815    /// just doesn't count what it can't read -- and does not call this.
1816    fn flatten_args_stat_numbers(
1817        &self,
1818        args: &[ResultData],
1819        is_direct: &[bool],
1820    ) -> Result<Vec<f64>, String> {
1821        let mut out = Vec::new();
1822        for (i, arg) in args.iter().enumerate() {
1823            let direct = is_direct.get(i).copied().unwrap_or(false);
1824            if direct && matches!(arg, ResultData::String(_)) && self.to_f64(arg).is_none() {
1825                return Err("#VALUE!".to_string());
1826            }
1827            out.extend(self.flatten_stat_numbers(arg, direct));
1828        }
1829        Ok(out)
1830    }
1831
1832    /// Flatten arguments for the `*A` statistical family (AVERAGEA, MAXA,
1833    /// MINA, STDEVA, STDEVPA, VARA, VARPA), which count text and booleans
1834    /// rather than skipping them.
1835    ///
1836    /// Text is where the family gets interesting, and the rule depends on
1837    /// *how* the text arrived. Inside a reference it counts as 0, which is
1838    /// the documented behaviour everyone knows. Passed directly as an
1839    /// argument it is coerced instead, and a value that will not coerce is
1840    /// an error rather than a zero. Against real Excel, with A1 holding the
1841    /// text "12":
1842    ///
1843    /// ```text
1844    /// AVERAGEA(A1, 3)     = 1.5        text in a reference counts as 0
1845    /// AVERAGEA("12", 3)   = 7.5        direct text is coerced
1846    /// AVERAGEA("abc", 3)  = #VALUE!    ... and must coerce
1847    /// ```
1848    fn flatten_stat_numbers_a(
1849        &self,
1850        arg: &ResultData,
1851        is_direct: bool,
1852    ) -> Result<Vec<f64>, String> {
1853        Ok(match arg {
1854            ResultData::Float(f) => vec![*f],
1855            ResultData::Integer(i) => vec![*i as f64],
1856            ResultData::Boolean(b) => vec![if *b { 1.0 } else { 0.0 }],
1857            ResultData::String(_) => {
1858                if is_direct {
1859                    match self.to_f64(arg) {
1860                        Some(f) => vec![f],
1861                        None => return Err("#VALUE!".to_string()),
1862                    }
1863                } else {
1864                    vec![0.0]
1865                }
1866            }
1867            ResultData::Error(e) => return Err(e.clone()),
1868            // Anything nested is a reference, never a direct argument.
1869            ResultData::List(list) => {
1870                let mut out = Vec::new();
1871                for v in list {
1872                    out.extend(self.flatten_stat_numbers_a(v, false)?);
1873                }
1874                out
1875            }
1876            ResultData::None => vec![],
1877            _ => vec![0.0],
1878        })
1879    }
1880
1881    /// `flatten_stat_numbers_a` over a whole argument list, using the
1882    /// caller's per-argument direct/reference classification.
1883    fn flatten_args_stat_numbers_a(
1884        &self,
1885        args: &[ResultData],
1886        is_direct: &[bool],
1887    ) -> Result<Vec<f64>, String> {
1888        let mut out = Vec::new();
1889        for (i, arg) in args.iter().enumerate() {
1890            out.extend(
1891                self.flatten_stat_numbers_a(arg, is_direct.get(i).copied().unwrap_or(false))?,
1892            );
1893        }
1894        Ok(out)
1895    }
1896
1897    fn extract_matrix(&self, arg: &ResultData) -> Vec<Vec<f64>> {
1898        match arg {
1899            ResultData::List(list) => {
1900                let mut rows = Vec::new();
1901                for item in list {
1902                    match item {
1903                        ResultData::List(sub_list) => {
1904                            let row: Vec<f64> =
1905                                sub_list.iter().flat_map(|v| self.to_f64(v)).collect();
1906                            if !row.is_empty() {
1907                                rows.push(row);
1908                            }
1909                        }
1910                        _ => {
1911                            if let Some(f) = self.to_f64(item) {
1912                                rows.push(vec![f]);
1913                            }
1914                        }
1915                    }
1916                }
1917                rows
1918            }
1919            _ => vec![],
1920        }
1921    }
1922
1923    /// Reshapes a range argument's flat evaluated list back into a 2D
1924    /// row-major matrix using the *reference's* own width.
1925    ///
1926    /// A plain rectangular range like `F1:G2` evaluates to a flat
1927    /// `List` of 4 scalars with no nesting, so extract_matrix (which can
1928    /// only treat a nested `List` as a row) turned it into a 4x1 column
1929    /// instead of a 2x2 square -- and every matrix function then reported
1930    /// #VALUE! on a perfectly valid square range. MMULT already
1931    /// reconstructed its operands' shapes from the argument expression
1932    /// this way; this shares that logic with MDETERM/MINVERSE.
1933    fn matrix_from_arg(
1934        &self,
1935        expr: &crate::core::parser::Expr,
1936        value: &ResultData,
1937    ) -> Vec<Vec<f64>> {
1938        // A list of lists already carries its own shape.
1939        if let ResultData::List(items) = value
1940            && items.iter().any(|i| matches!(i, ResultData::List(_)))
1941        {
1942            return self.extract_matrix(value);
1943        }
1944        // Only real numbers: the matrix functions reject text, booleans
1945        // and blanks alike (all confirmed #VALUE! against real Excel), so
1946        // a cell that isn't a number collapses the whole matrix rather
1947        // than being coerced by to_f64.
1948        fn plain(v: &ResultData) -> Option<f64> {
1949            match v {
1950                ResultData::Float(f) => Some(*f),
1951                ResultData::Integer(i) => Some(*i as f64),
1952                _ => None,
1953            }
1954        }
1955        let items: Vec<&ResultData> = match value {
1956            ResultData::List(items) => items.iter().collect(),
1957            other => vec![other],
1958        };
1959        if items.iter().any(|v| plain(v).is_none()) {
1960            return Vec::new();
1961        }
1962        let flat: Vec<f64> = items.iter().filter_map(|v| plain(v)).collect();
1963        let cols = match Self::range_bounds(expr) {
1964            Some((_, _, start_col, _, end_col)) => end_col.saturating_sub(start_col) + 1,
1965            None => flat.len().max(1),
1966        };
1967        if cols == 0 || !flat.len().is_multiple_of(cols) {
1968            return self.extract_matrix(value);
1969        }
1970        flat.chunks(cols).map(|c| c.to_vec()).collect()
1971    }
1972
1973    /// An optional numeric argument. An *absent* argument falls back to
1974    /// `default`, but one that is present and non-numeric is #VALUE! --
1975    /// the `.and_then(to_f64).unwrap_or(default)` shape used in places
1976    /// conflates the two, so e.g. `LOG(3.14, "E")` quietly computed
1977    /// base-10 instead of erroring.
1978    /// `#DIV/0!` when either operand of a paired sum contains no numeric
1979    /// value at all.
1980    ///
1981    /// This is *not* the same as "no pair survived exclusion", which is
1982    /// simply 0. Real Excel, with a column [53, TRUE] against a row
1983    /// [TRUE, -10]: every pair is dropped (each holds a boolean), yet the
1984    /// answer is 0 rather than an error, because each range does hold a
1985    /// number. Swap in a range that is entirely text or entirely booleans
1986    /// and it becomes #DIV/0!.
1987    ///
1988    /// Fitted against eleven real-Excel cases spanning text, booleans and
1989    /// mixtures, at one, two and three elements per range.
1990    fn paired_sum_has_no_numbers(&self, arg: Option<&ResultData>) -> bool {
1991        let mut ignored = None;
1992        let slots = self.positional_numbers(arg, &mut ignored);
1993        slots.iter().all(|v| v.is_none())
1994    }
1995
1996    /// True when an argument is a *single-cell* operand that is empty.
1997    ///
1998    /// Excel treats that as a missing operand and answers #VALUE!, rather
1999    /// than as a one-element array of nothing. The distinction is
2000    /// specifically about a single cell: `SUMPRODUCT(<one blank cell>)` is
2001    /// #VALUE! while `SUMPRODUCT(<two blank cells>)` is 0, and
2002    /// `SUMPRODUCT(-50, <blank>)` is #VALUE! too. Same for MULTINOMIAL and
2003    /// the paired statistical functions.
2004    ///
2005    /// A one-cell range evaluates to a one-element `List` rather than a
2006    /// bare scalar, so both spellings have to be unwrapped. Note this is
2007    /// about blankness only -- a one-cell operand holding text or a
2008    /// boolean behaves differently again.
2009    fn is_empty_scalar_operand(arg: &ResultData) -> bool {
2010        let scalar = match arg {
2011            ResultData::List(items) if items.len() == 1 => &items[0],
2012            other => other,
2013        };
2014        matches!(scalar, ResultData::None)
2015    }
2016
2017    /// True when the first argument is a boolean and the function is one
2018    /// of the few that refuse them.
2019    ///
2020    /// Excel's numeric coercion is not uniform here. SQRT, FACT, SIGN,
2021    /// INT, EXP, ROMAN and most of their neighbours take TRUE as 1
2022    /// without complaint, but ERF, ERFC, FACTDOUBLE and SQRTPI all answer
2023    /// #VALUE! -- verified one function at a time against real Excel,
2024    /// because the split does not follow from anything about the
2025    /// functions themselves.
2026    fn first_arg_is_boolean(args: &[ResultData]) -> bool {
2027        matches!(args.first(), Some(ResultData::Boolean(_)))
2028    }
2029
2030    fn opt_f64_arg(&self, args: &[ResultData], i: usize, default: f64) -> Result<f64, EngineError> {
2031        match args.get(i) {
2032            None => Ok(default),
2033            // A supplied-but-blank argument is 0, not the default. Excel
2034            // draws that line sharply: LOG(1, <blank>) is #NUM! because the
2035            // base is 0, while LOG(1) uses base 10 and returns 0. Same for
2036            // LEFT("abcd", <blank>) = "" and MROUND(10, <blank>) = 0.
2037            Some(ResultData::None) => Ok(0.0),
2038            Some(ResultData::Error(e)) => Err(EngineError::EvalError(EvalError::UnknownFunction(
2039                e.clone(),
2040            ))),
2041            Some(v) => self.to_f64(v).ok_or_else(|| {
2042                EngineError::EvalError(EvalError::UnknownFunction("#VALUE!".to_string()))
2043            }),
2044        }
2045    }
2046
2047    fn opt_f64(&self, args: &[ResultData], i: usize, default: f64) -> f64 {
2048        args.get(i).and_then(|v| self.to_f64(v)).unwrap_or(default)
2049    }
2050
2051    fn average_helper(&self, arg: &ResultData, is_direct: bool) -> (f64, usize) {
2052        match arg {
2053            ResultData::Float(f) => (*f, 1),
2054            ResultData::Integer(i) => (*i as f64, 1),
2055            ResultData::Boolean(b) => {
2056                if is_direct {
2057                    (if *b { 1.0 } else { 0.0 }, 1)
2058                } else {
2059                    (0.0, 0)
2060                }
2061            }
2062            ResultData::String(_) => {
2063                if is_direct {
2064                    if let Some(f) = self.to_f64(arg) {
2065                        (f, 1)
2066                    } else {
2067                        (0.0, 0)
2068                    }
2069                } else {
2070                    (0.0, 0)
2071                }
2072            }
2073            ResultData::List(list) => {
2074                let mut sum = 0.0;
2075                let mut count = 0;
2076                for item in list {
2077                    let (s, c) = self.average_helper(item, false);
2078                    sum += s;
2079                    count += c;
2080                }
2081                (sum, count)
2082            }
2083            _ => (0.0, 0),
2084        }
2085    }
2086
2087    fn count_helper(&self, arg: &ResultData) -> usize {
2088        match arg {
2089            ResultData::Float(_) | ResultData::Integer(_) => 1,
2090            ResultData::List(list) => {
2091                let mut count = 0;
2092                for item in list {
2093                    count += self.count_helper(item);
2094                }
2095                count
2096            }
2097            _ => 0,
2098        }
2099    }
2100
2101    fn min_helper(&self, arg: &ResultData, is_direct: bool) -> f64 {
2102        match arg {
2103            ResultData::Float(f) => *f,
2104            ResultData::Integer(i) => *i as f64,
2105            ResultData::Boolean(b) => {
2106                if is_direct {
2107                    if *b { 1.0 } else { 0.0 }
2108                } else {
2109                    f64::INFINITY
2110                }
2111            }
2112            ResultData::String(_) => {
2113                if is_direct {
2114                    self.to_f64(arg).unwrap_or(f64::INFINITY)
2115                } else {
2116                    f64::INFINITY
2117                }
2118            }
2119            ResultData::List(list) => {
2120                let mut min_val = f64::INFINITY;
2121                for item in list {
2122                    min_val = min_val.min(self.min_helper(item, false));
2123                }
2124                min_val
2125            }
2126            _ => f64::INFINITY,
2127        }
2128    }
2129
2130    fn max_helper(&self, arg: &ResultData, is_direct: bool) -> f64 {
2131        match arg {
2132            ResultData::Float(f) => *f,
2133            ResultData::Integer(i) => *i as f64,
2134            ResultData::Boolean(b) => {
2135                if is_direct {
2136                    if *b { 1.0 } else { 0.0 }
2137                } else {
2138                    f64::NEG_INFINITY
2139                }
2140            }
2141            ResultData::String(_) => {
2142                if is_direct {
2143                    self.to_f64(arg).unwrap_or(f64::NEG_INFINITY)
2144                } else {
2145                    f64::NEG_INFINITY
2146                }
2147            }
2148            ResultData::List(list) => {
2149                let mut max_val = f64::NEG_INFINITY;
2150                for item in list {
2151                    max_val = max_val.max(self.max_helper(item, false));
2152                }
2153                max_val
2154            }
2155            _ => f64::NEG_INFINITY,
2156        }
2157    }
2158
2159    fn concat_helper(&self, arg: &ResultData, out: &mut String) {
2160        match arg {
2161            ResultData::List(list) => {
2162                for item in list {
2163                    self.concat_helper(item, out);
2164                }
2165            }
2166            other => {
2167                out.push_str(&other.to_string());
2168            }
2169        }
2170    }
2171
2172    fn counta_helper(&self, arg: &ResultData) -> usize {
2173        match arg {
2174            ResultData::None => 0,
2175            // COUNTA counts every non-blank value, and the empty string is a
2176            // value -- Excel counts both a text cell holding "" and a formula
2177            // that returned "".
2178            ResultData::List(list) => {
2179                let mut count = 0;
2180                for item in list {
2181                    count += self.counta_helper(item);
2182                }
2183                count
2184            }
2185            _ => 1,
2186        }
2187    }
2188
2189    fn product_helper(&self, arg: &ResultData, is_direct: bool) -> (f64, bool) {
2190        match arg {
2191            ResultData::Float(f) => (*f, true),
2192            ResultData::Integer(i) => (*i as f64, true),
2193            ResultData::Boolean(b) => {
2194                if is_direct {
2195                    (if *b { 1.0 } else { 0.0 }, true)
2196                } else {
2197                    (1.0, false)
2198                }
2199            }
2200            ResultData::String(_) => {
2201                if is_direct {
2202                    if let Some(f) = self.to_f64(arg) {
2203                        (f, true)
2204                    } else {
2205                        (1.0, false)
2206                    }
2207                } else {
2208                    (1.0, false)
2209                }
2210            }
2211            ResultData::List(list) => {
2212                let mut prod = 1.0;
2213                let mut has_nums = false;
2214                for item in list {
2215                    let (p, h) = self.product_helper(item, false);
2216                    if h {
2217                        // Raw here; the 15-significant-digit snap belongs
2218                        // on the final product only. See the PRODUCT arm.
2219                        prod *= p;
2220                        has_nums = true;
2221                    }
2222                }
2223                (prod, has_nums)
2224            }
2225            _ => (1.0, false),
2226        }
2227    }
2228
2229    fn to_bool_opt(&self, val: &ResultData) -> Option<bool> {
2230        match val {
2231            ResultData::Boolean(b) => Some(*b),
2232            ResultData::Integer(i) => Some(*i != 0),
2233            ResultData::Float(f) => Some(*f != 0.0),
2234            ResultData::String(s) => {
2235                let s_trim = s.trim();
2236                if s_trim.eq_ignore_ascii_case("true") {
2237                    Some(true)
2238                } else if s_trim.eq_ignore_ascii_case("false") {
2239                    Some(false)
2240                } else if let Ok(f) = s_trim.parse::<f64>() {
2241                    Some(f != 0.0)
2242                } else {
2243                    None
2244                }
2245            }
2246            ResultData::None => Some(false),
2247            _ => None,
2248        }
2249    }
2250
2251    fn to_bool(&self, val: &ResultData) -> bool {
2252        self.to_bool_opt(val).unwrap_or(false)
2253    }
2254
2255    /// Strict "is this a genuine number" check for range-value aggregation
2256    /// (DCOUNT/DSUM/DAVERAGE/... and friends), as opposed to `to_f64`'s
2257    /// scalar-arithmetic coercion (which maps blank -> 0 and booleans ->
2258    /// 1/0). Confirmed against real Excel via the differential fuzzer that
2259    /// blank and boolean database cells must be excluded here the same
2260    /// way SUM/COUNT/AVERAGE ignore them within a range argument -- using
2261    /// `to_f64` instead let a blank row zero out DPRODUCT entirely and
2262    /// skewed DCOUNT/DSUM/DAVERAGE by counting/summing blanks and
2263    /// TRUE/FALSE as 0/1.
2264    fn range_numeric(val: &ResultData) -> Option<f64> {
2265        match val {
2266            ResultData::Integer(i) => Some(*i as f64),
2267            ResultData::Float(f) => Some(*f),
2268            _ => None,
2269        }
2270    }
2271
2272    /// Exact-match ("match_type 0" / "range_lookup FALSE") comparison for
2273    /// MATCH/VLOOKUP/HLOOKUP/XLOOKUP.
2274    ///
2275    /// A *blank* lookup value is coerced to 0 (Excel's usual empty-cell
2276    /// coercion) and a blank cell in the searched range never matches
2277    /// anything. Comparing the two blanks as equal strings instead --
2278    /// which is what a plain `to_string()` comparison does, since both
2279    /// render as "" -- made `MATCH(A1, A1:A4, 0)` over a blank A1 report
2280    /// a hit at position 1 where real Excel reports #N/A.
2281    fn exact_lookup_matches(lookup: &ResultData, candidate: &ResultData) -> bool {
2282        if matches!(candidate, ResultData::None) {
2283            return false;
2284        }
2285        let lookup_key = match lookup {
2286            ResultData::None => "0".to_string(),
2287            other => other.to_string(),
2288        };
2289        candidate.to_string() == lookup_key
2290    }
2291
2292    fn wildcard_criteria_matches(pattern: &str, text: &str) -> bool {
2293        fn rec(pat: &[char], txt: &[char]) -> bool {
2294            if pat.is_empty() {
2295                return txt.is_empty();
2296            }
2297            match pat[0] {
2298                '*' => rec(&pat[1..], txt) || (!txt.is_empty() && rec(pat, &txt[1..])),
2299                '?' => !txt.is_empty() && rec(&pat[1..], &txt[1..]),
2300                '~' if pat.len() > 1 && matches!(pat[1], '*' | '?' | '~') => {
2301                    !txt.is_empty() && pat[1] == txt[0] && rec(&pat[2..], &txt[1..])
2302                }
2303                ch => !txt.is_empty() && ch == txt[0] && rec(&pat[1..], &txt[1..]),
2304            }
2305        }
2306
2307        let pat = pattern.to_lowercase().chars().collect::<Vec<_>>();
2308        let txt = text.to_lowercase().chars().collect::<Vec<_>>();
2309        rec(&pat, &txt)
2310    }
2311
2312    fn criteria_text_eq(val: &ResultData, pattern: &str) -> bool {
2313        let text = val.to_string();
2314        if pattern.contains('*') || pattern.contains('?') {
2315            // Excel wildcard criteria are text-pattern matches; numeric and
2316            // boolean cells are not counted by criteria like "*".
2317            matches!(val, ResultData::String(_)) && Self::wildcard_criteria_matches(pattern, &text)
2318        } else {
2319            text.to_lowercase() == pattern.to_lowercase()
2320        }
2321    }
2322
2323    fn match_criteria(&self, val: &ResultData, criteria: &ResultData) -> bool {
2324        let crit_str = criteria.to_string();
2325        if let Some(rest) = crit_str.strip_prefix(">=") {
2326            // A numeric comparison can only ever be satisfied by a genuine
2327            // number -- confirmed against real Excel via the differential
2328            // fuzzer (fuzzing the new database D* functions): blank, text,
2329            // and boolean cells must all fail ">"/"<" criteria outright,
2330            // not fall back to comparing as if they were 0.
2331            let val_f = match Self::range_numeric(val) {
2332                Some(f) => f,
2333                None => return false,
2334            };
2335            let crit_f = rest.trim().parse::<f64>().unwrap_or(0.0);
2336            val_f >= crit_f
2337        } else if let Some(rest) = crit_str.strip_prefix('>') {
2338            let val_f = match Self::range_numeric(val) {
2339                Some(f) => f,
2340                None => return false,
2341            };
2342            let crit_f = rest.trim().parse::<f64>().unwrap_or(0.0);
2343            val_f > crit_f
2344        } else if let Some(rest) = crit_str.strip_prefix("<>") {
2345            let remainder = rest.trim();
2346            !Self::criteria_text_eq(val, remainder)
2347        } else if let Some(rest) = crit_str.strip_prefix("<=") {
2348            let val_f = match Self::range_numeric(val) {
2349                Some(f) => f,
2350                None => return false,
2351            };
2352            let crit_f = rest.trim().parse::<f64>().unwrap_or(0.0);
2353            val_f <= crit_f
2354        } else if let Some(rest) = crit_str.strip_prefix('<') {
2355            let val_f = match Self::range_numeric(val) {
2356                Some(f) => f,
2357                None => return false,
2358            };
2359            let crit_f = rest.trim().parse::<f64>().unwrap_or(0.0);
2360            val_f < crit_f
2361        } else if let Some(rest) = crit_str.strip_prefix('=') {
2362            let remainder = rest.trim();
2363            Self::criteria_text_eq(val, remainder)
2364        } else {
2365            Self::criteria_text_eq(val, &crit_str)
2366        }
2367    }
2368
2369    /// Resolves an argument `Expr` to its raw `(sheet, start_row, start_col,
2370    /// end_row, end_col)` range bounds, for functions (like the database
2371    /// `D*` family below) that need genuine 2D shape and can't work off the
2372    /// pre-flattened `ResultData::List` every other argument already went
2373    /// through in `evaluated_args`.
2374    fn range_bounds(
2375        expr: &crate::core::parser::Expr,
2376    ) -> Option<(Option<String>, usize, usize, usize, usize)> {
2377        use crate::core::parser::Expr;
2378        match expr {
2379            Expr::RangeRef {
2380                sheet,
2381                start_row,
2382                start_col,
2383                end_row,
2384                end_col,
2385                ..
2386            } => Some((sheet.clone(), *start_row, *start_col, *end_row, *end_col)),
2387            Expr::CellRef {
2388                sheet, row, col, ..
2389            } => Some((sheet.clone(), *row, *col, *row, *col)),
2390            _ => None,
2391        }
2392    }
2393
2394    /// Reads a range's cells into a row-major grid, resolving a whole-column
2395    /// range's `end_row` sentinel and cross-sheet references via `context`.
2396    /// Materializing into an owned `Vec<Vec<ResultData>>` (rather than
2397    /// keeping a live `&Sheet` around) sidesteps the local-vs-remote
2398    /// lifetime split for the rest of the database-function logic, and
2399    /// database/criteria ranges are small enough that this is cheap.
2400    fn materialize_range(
2401        &self,
2402        sheet_opt: &Option<String>,
2403        start_row: usize,
2404        start_col: usize,
2405        end_row: usize,
2406        end_col: usize,
2407        context: Option<&Context>,
2408    ) -> Option<Vec<Vec<ResultData>>> {
2409        let is_self = match sheet_opt {
2410            Some(name) => name == &self.name,
2411            None => true,
2412        };
2413        let source: &Sheet = if is_self {
2414            self
2415        } else {
2416            context?.sheets.get(sheet_opt.as_ref()?)?
2417        };
2418        let actual_end_row = if end_row == usize::MAX {
2419            source.row_count().saturating_sub(1)
2420        } else {
2421            end_row
2422        };
2423        let actual_end_col = if end_col == usize::MAX {
2424            source.col_count().saturating_sub(1)
2425        } else {
2426            end_col
2427        };
2428        if actual_end_row < start_row || actual_end_col < start_col {
2429            return Some(Vec::new());
2430        }
2431        let mut grid = Vec::with_capacity(actual_end_row - start_row + 1);
2432        for r in start_row..=actual_end_row {
2433            let mut row = Vec::with_capacity(actual_end_col - start_col + 1);
2434            for c in start_col..=actual_end_col {
2435                row.push(source.get_result_data(&CellRef::new(r, c)));
2436            }
2437            grid.push(row);
2438        }
2439        Some(grid)
2440    }
2441
2442    /// Shared implementation for the 12 database `D*` functions
2443    /// (DAVERAGE/DCOUNT/DCOUNTA/DGET/DMAX/DMIN/DPRODUCT/DSTDEV/DSTDEVP/
2444    /// DSUM/DVAR/DVARP): each reduces to "match database rows against the
2445    /// criteria table, then aggregate one field column of the matches" --
2446    /// they differ only in which aggregation runs at the end.
2447    ///
2448    /// `database`/`criteria` are read from the raw `args` AST nodes (not
2449    /// `evaluated_args`) specifically to recover real row/column bounds;
2450    /// `field` (name or 1-based index) still comes from `evaluated_args`
2451    /// since it's a scalar. Criteria semantics match Excel's: multiple
2452    /// criteria *rows* are OR'd together, multiple non-blank cells within
2453    /// one criteria row are AND'd, and a blank criteria cell imposes no
2454    /// constraint on that field.
2455    fn evaluate_database_function(
2456        &self,
2457        func_name: &str,
2458        args: &[crate::core::parser::Expr],
2459        evaluated_args: &[ResultData],
2460        context: Option<&Context>,
2461    ) -> Result<ResultData, EngineError> {
2462        if args.len() < 3 || evaluated_args.len() < 3 {
2463            return Ok(ResultData::Error("#VALUE!".to_string()));
2464        }
2465        let (db_sheet, db_sr, db_sc, db_er, db_ec) = match Self::range_bounds(&args[0]) {
2466            Some(v) => v,
2467            None => return Ok(ResultData::Error("#VALUE!".to_string())),
2468        };
2469        let (crit_sheet, crit_sr, crit_sc, crit_er, crit_ec) = match Self::range_bounds(&args[2]) {
2470            Some(v) => v,
2471            None => return Ok(ResultData::Error("#VALUE!".to_string())),
2472        };
2473        let db = match self.materialize_range(&db_sheet, db_sr, db_sc, db_er, db_ec, context) {
2474            Some(g) => g,
2475            None => return Ok(ResultData::Error("#REF!".to_string())),
2476        };
2477        let crit = match self.materialize_range(
2478            &crit_sheet,
2479            crit_sr,
2480            crit_sc,
2481            crit_er,
2482            crit_ec,
2483            context,
2484        ) {
2485            Some(g) => g,
2486            None => return Ok(ResultData::Error("#REF!".to_string())),
2487        };
2488        if db.len() < 2 || crit.len() < 2 {
2489            return Ok(ResultData::Error("#VALUE!".to_string()));
2490        }
2491
2492        let db_headers: Vec<String> = db[0].iter().map(|v| v.to_string()).collect();
2493        let field_idx: usize = match &evaluated_args[1] {
2494            ResultData::String(s) => {
2495                match db_headers.iter().position(|h| h.eq_ignore_ascii_case(s)) {
2496                    Some(idx) => idx,
2497                    None => return Ok(ResultData::Error("#VALUE!".to_string())),
2498                }
2499            }
2500            other => match self.to_f64(other) {
2501                Some(n) if n >= 1.0 && (n as usize) <= db_headers.len() => n as usize - 1,
2502                _ => return Ok(ResultData::Error("#VALUE!".to_string())),
2503            },
2504        };
2505
2506        let crit_headers: Vec<String> = crit[0].iter().map(|v| v.to_string()).collect();
2507        let crit_to_db: Vec<Option<usize>> = crit_headers
2508            .iter()
2509            .map(|h| db_headers.iter().position(|dh| dh.eq_ignore_ascii_case(h)))
2510            .collect();
2511
2512        let mut matched: Vec<ResultData> = Vec::new();
2513        for row in db.iter().skip(1) {
2514            let row_matches_any_criteria_row = crit.iter().skip(1).any(|crit_row| {
2515                crit_row.iter().enumerate().all(|(ci, cell)| {
2516                    if matches!(cell, ResultData::None) {
2517                        return true;
2518                    }
2519                    match crit_to_db.get(ci).copied().flatten() {
2520                        Some(db_col) => self.match_criteria(&row[db_col], cell),
2521                        None => false,
2522                    }
2523                })
2524            });
2525            if row_matches_any_criteria_row {
2526                matched.push(row[field_idx].clone());
2527            }
2528        }
2529
2530        match func_name {
2531            "DGET" => match matched.len() {
2532                0 => Ok(ResultData::Error("#VALUE!".to_string())),
2533                1 => Ok(matched.into_iter().next().unwrap()),
2534                _ => Ok(ResultData::Error("#NUM!".to_string())),
2535            },
2536            "DCOUNT" => Ok(ResultData::Float(
2537                matched
2538                    .iter()
2539                    .filter(|v| Self::range_numeric(v).is_some())
2540                    .count() as f64,
2541            )),
2542            "DCOUNTA" => Ok(ResultData::Float(
2543                matched.iter().map(|v| self.counta_helper(v)).sum::<usize>() as f64,
2544            )),
2545            _ => {
2546                let nums: Vec<f64> = matched.iter().filter_map(Self::range_numeric).collect();
2547                match func_name {
2548                    "DSUM" => Ok(ResultData::Float(nums.iter().sum())),
2549                    "DPRODUCT" => Ok(ResultData::Float(if nums.is_empty() {
2550                        0.0
2551                    } else {
2552                        nums.iter().product()
2553                    })),
2554                    "DMAX" => {
2555                        let m = nums.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
2556                        Ok(ResultData::Float(if m.is_finite() { m } else { 0.0 }))
2557                    }
2558                    "DMIN" => {
2559                        let m = nums.iter().cloned().fold(f64::INFINITY, f64::min);
2560                        Ok(ResultData::Float(if m.is_finite() { m } else { 0.0 }))
2561                    }
2562                    "DAVERAGE" => {
2563                        if nums.is_empty() {
2564                            Ok(ResultData::Error("#DIV/0!".to_string()))
2565                        } else {
2566                            Ok(ResultData::Float(
2567                                nums.iter().sum::<f64>() / nums.len() as f64,
2568                            ))
2569                        }
2570                    }
2571                    "DSTDEV" => match crate::core::stats::stdev_s(&nums) {
2572                        Ok(v) => Ok(ResultData::Float(v)),
2573                        Err(e) => Ok(ResultData::Error(e)),
2574                    },
2575                    "DSTDEVP" => match crate::core::stats::stdev_p(&nums) {
2576                        Ok(v) => Ok(ResultData::Float(v)),
2577                        Err(e) => Ok(ResultData::Error(e)),
2578                    },
2579                    "DVAR" => match crate::core::stats::var_s(&nums) {
2580                        Ok(v) => Ok(ResultData::Float(v)),
2581                        Err(e) => Ok(ResultData::Error(e)),
2582                    },
2583                    "DVARP" => match crate::core::stats::var_p(&nums) {
2584                        Ok(v) => Ok(ResultData::Float(v)),
2585                        Err(e) => Ok(ResultData::Error(e)),
2586                    },
2587                    _ => unreachable!(),
2588                }
2589            }
2590        }
2591    }
2592
2593    fn proper(&self, s: &str) -> String {
2594        // Per Microsoft's own definition, PROPER capitalizes a letter
2595        // preceded by "any character that is not a letter" -- that
2596        // includes digits, not just punctuation/spacing, which is why
2597        // PROPER("123abc") is "123Abc": the digits aren't letters, so the
2598        // 'a' right after them still counts as the start of a new word.
2599        let mut c_chars = Vec::new();
2600        let mut capitalize_next = true;
2601        for c in s.chars() {
2602            if c.is_alphabetic() {
2603                if capitalize_next {
2604                    c_chars.extend(c.to_uppercase());
2605                } else {
2606                    c_chars.extend(c.to_lowercase());
2607                }
2608                capitalize_next = false;
2609            } else {
2610                c_chars.push(c);
2611                capitalize_next = true;
2612            }
2613        }
2614        c_chars.into_iter().collect()
2615    }
2616
2617    fn get_ymd_hms(&self) -> ((i32, u32, u32), (u32, u32, u32)) {
2618        let now = web_time::SystemTime::now()
2619            .duration_since(web_time::SystemTime::UNIX_EPOCH)
2620            .unwrap_or_default()
2621            .as_secs();
2622        let secs_in_day = 86400;
2623        let days_since_epoch = (now / secs_in_day) as i32;
2624        let seconds_of_day = (now % secs_in_day) as u32;
2625
2626        let hour = seconds_of_day / 3600;
2627        let minute = (seconds_of_day % 3600) / 60;
2628        let second = seconds_of_day % 60;
2629
2630        let era = (if days_since_epoch >= -719468 {
2631            days_since_epoch + 719468
2632        } else {
2633            days_since_epoch + 719468 - 146096
2634        }) / 146097;
2635        let doe = (days_since_epoch + 719468 - era * 146097) as u32;
2636        let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
2637        let y = (yoe as i32) + era * 400;
2638        let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2639        let mp = (5 * doy + 2) / 153;
2640        let d = doy - (153 * mp + 2) / 5 + 1;
2641        let m = if mp < 10 { mp + 3 } else { mp - 9 };
2642        let year = if m <= 2 { y + 1 } else { y };
2643
2644        ((year, m, d), (hour, minute, second))
2645    }
2646
2647    /// Evaluates Excel's LET(name1, value1, [name2, value2, ...],
2648    /// calculation). Binds each name/value pair in order -- value2 (and
2649    /// later pairs, and the final calculation) can reference name1, per
2650    /// Excel's LET semantics -- by recursing one pair at a time so each
2651    /// level's scope chain only needs to borrow the *previous* level's
2652    /// binding rather than mutate a shared map (see `LetScope`).
2653    fn evaluate_let(
2654        &self,
2655        args: &[crate::core::parser::Expr],
2656        context: Option<&Context>,
2657        row: Option<usize>,
2658        col: Option<usize>,
2659        deps: &mut Vec<Dependency>,
2660        scope: &LetScope<'_>,
2661    ) -> Result<ResultData, EngineError> {
2662        use crate::core::parser::Expr;
2663
2664        if args.is_empty() || args.len().is_multiple_of(2) {
2665            // Needs one or more name/value pairs followed by a calculation,
2666            // i.e. an odd number of arguments overall.
2667            return Ok(ResultData::Error("#VALUE!".to_string()));
2668        }
2669        if args.len() == 1 {
2670            return self.evaluate_ast(&args[0], context, row, col, deps, scope);
2671        }
2672
2673        let name = match &args[0] {
2674            Expr::Identifier(n) => n.as_str(),
2675            _ => return Ok(ResultData::Error("#VALUE!".to_string())),
2676        };
2677        // Excel rejects reusing a name across a single LET's own pairs,
2678        // rather than letting a later pair silently shadow an earlier one.
2679        let remaining_pairs = args.len() / 2 - 1;
2680        let is_duplicate = args[2..]
2681            .iter()
2682            .step_by(2)
2683            .take(remaining_pairs)
2684            .any(|a| matches!(a, Expr::Identifier(n2) if n2.eq_ignore_ascii_case(name)));
2685        if is_duplicate {
2686            return Ok(ResultData::Error("#VALUE!".to_string()));
2687        }
2688
2689        let value = self.evaluate_ast(&args[1], context, row, col, deps, scope)?;
2690        let inner_scope = LetScope::Bound {
2691            name,
2692            value: &value,
2693            parent: scope,
2694        };
2695        self.evaluate_let(&args[2..], context, row, col, deps, &inner_scope)
2696    }
2697
2698    /// Recognizes `expr` as a `LAMBDA(param1, [param2, ...], body)` call
2699    /// and, if so, returns its declared parameter names alongside the
2700    /// (still-unevaluated) body expression. Used by every function below
2701    /// that takes a lambda argument: the lambda is never evaluated as an
2702    /// ordinary function call (there's no value a bare LAMBDA could
2703    /// produce on its own -- see the `#CALC!` case in `evaluate_function`)
2704    /// -- callers instead inspect its raw AST here and invoke the body
2705    /// themselves, once per element, via `invoke_lambda`.
2706    fn extract_lambda(
2707        expr: &crate::core::parser::Expr,
2708    ) -> Option<(Vec<&str>, &crate::core::parser::Expr)> {
2709        use crate::core::parser::Expr;
2710        let Expr::FunctionCall { name, args } = expr else {
2711            return None;
2712        };
2713        if !name.eq_ignore_ascii_case("LAMBDA") || args.is_empty() {
2714            return None;
2715        }
2716        let (body, params) = args.split_last().unwrap();
2717        let param_names: Vec<&str> = params
2718            .iter()
2719            .filter_map(|p| match p {
2720                Expr::Identifier(n) => Some(n.as_str()),
2721                _ => None,
2722            })
2723            .collect();
2724        if param_names.len() != params.len() {
2725            return None;
2726        }
2727        Some((param_names, body))
2728    }
2729
2730    /// Evaluates a lambda's body with each of `params` bound (via
2731    /// `LetScope`) to the corresponding entry of `values`, which must be
2732    /// the same length. `values` is borrowed rather than consumed so
2733    /// callers can reuse per-element storage across many invocations
2734    /// (e.g. MAP calling this once per array element).
2735    #[allow(clippy::too_many_arguments)]
2736    fn invoke_lambda<'v>(
2737        &self,
2738        params: &[&str],
2739        values: &'v [ResultData],
2740        body: &crate::core::parser::Expr,
2741        context: Option<&Context>,
2742        row: Option<usize>,
2743        col: Option<usize>,
2744        deps: &mut Vec<Dependency>,
2745        scope: &LetScope<'v>,
2746    ) -> Result<ResultData, EngineError> {
2747        match (params.split_first(), values.split_first()) {
2748            (Some((&pname, prest)), Some((vfirst, vrest))) => {
2749                let inner_scope = LetScope::Bound {
2750                    name: pname,
2751                    value: vfirst,
2752                    parent: scope,
2753                };
2754                self.invoke_lambda(prest, vrest, body, context, row, col, deps, &inner_scope)
2755            }
2756            _ => self.evaluate_ast(body, context, row, col, deps, scope),
2757        }
2758    }
2759
2760    /// Flattens `expr` (evaluated) into a `Vec<ResultData>`, treating a
2761    /// scalar as a single-element array -- shared by MAP/REDUCE/SCAN,
2762    /// which all iterate an "array" argument that might just be one cell.
2763    fn eval_as_array(
2764        &self,
2765        expr: &crate::core::parser::Expr,
2766        context: Option<&Context>,
2767        row: Option<usize>,
2768        col: Option<usize>,
2769        deps: &mut Vec<Dependency>,
2770        scope: &LetScope<'_>,
2771    ) -> Result<Vec<ResultData>, EngineError> {
2772        Ok(
2773            match self.evaluate_ast(expr, context, row, col, deps, scope)? {
2774                ResultData::List(items) => Self::flatten_row_major(items).0,
2775                other => vec![other],
2776            },
2777        )
2778    }
2779
2780    /// `SEQUENCE`/`MUNIT` (unlike every array-*reshaping* function added
2781    /// this session) return their 2D result as a genuinely nested
2782    /// `List(List(row_values), ...)`, one inner list per row, rather than
2783    /// a flat row-major list -- that's the only place in this engine a
2784    /// `ResultData::List` still carries real shape. Detect that shape
2785    /// here and flatten it so downstream consumers (`array_shape`,
2786    /// `INDEX`, reshape functions) don't need to special-case it; a list
2787    /// that isn't uniformly nested (the flat convention) passes through
2788    /// unchanged, with `None` signaling "no shape recovered here".
2789    fn flatten_row_major(items: Vec<ResultData>) -> (Vec<ResultData>, Option<usize>) {
2790        if !items.is_empty() && items.iter().all(|v| matches!(v, ResultData::List(_))) {
2791            let cols = match &items[0] {
2792                ResultData::List(inner) => inner.len().max(1),
2793                _ => 1,
2794            };
2795            let flat = items
2796                .into_iter()
2797                .flat_map(|v| match v {
2798                    ResultData::List(inner) => inner,
2799                    other => vec![other],
2800                })
2801                .collect();
2802            (flat, Some(cols))
2803        } else {
2804            (items, None)
2805        }
2806    }
2807
2808    /// Infers `(flat_values, num_cols)` for an array-like argument: real
2809    /// column count from a `RangeRef`/`CellRef` AST node when available,
2810    /// otherwise treats the flattened result as a single row -- the same
2811    /// convention `INDEX`'s 3-arg form already uses (see its `num_cols`
2812    /// match on `args[0]`), since a computed/nested array result (e.g. the
2813    /// output of another array function) carries no shape of its own in
2814    /// this engine's flat-`ResultData::List` representation.
2815    fn array_shape(
2816        &self,
2817        expr: &crate::core::parser::Expr,
2818        context: Option<&Context>,
2819        row: Option<usize>,
2820        col: Option<usize>,
2821        deps: &mut Vec<Dependency>,
2822        scope: &LetScope<'_>,
2823    ) -> Result<(Vec<ResultData>, usize), EngineError> {
2824        use crate::core::parser::Expr;
2825        let items = match self.evaluate_ast(expr, context, row, col, deps, scope)? {
2826            ResultData::List(items) => items,
2827            other => vec![other],
2828        };
2829        let (flat, nested_cols) = Self::flatten_row_major(items);
2830        if let Some(cols) = nested_cols {
2831            return Ok((flat, cols));
2832        }
2833        let num_cols = match expr {
2834            Expr::RangeRef {
2835                start_col, end_col, ..
2836            } => (end_col - start_col + 1).max(1),
2837            Expr::CellRef { .. } => 1,
2838            Expr::FunctionCall { name, args } => self
2839                .function_call_cols(name, args, context, row, col, deps, scope)
2840                .unwrap_or_else(|| flat.len().max(1)),
2841            _ => flat.len().max(1),
2842        };
2843        Ok((flat, num_cols))
2844    }
2845
2846    /// Recovers the column count an array-reshaping function call's result
2847    /// would have, purely from its argument expressions -- needed because
2848    /// this engine's flat `ResultData::List` carries no shape of its own,
2849    /// so nesting one of these calls inside another (e.g.
2850    /// `INDEX(EXPAND(A1:B2,3,3,0),3,3)`) previously fell back to treating
2851    /// the whole result as a single row, corrupting the flat-index math.
2852    /// Returns `None` for anything not in this known set, so callers fall
2853    /// back to the single-row assumption.
2854    #[allow(clippy::too_many_arguments)]
2855    fn function_call_cols(
2856        &self,
2857        name: &str,
2858        args: &[crate::core::parser::Expr],
2859        context: Option<&Context>,
2860        row: Option<usize>,
2861        col: Option<usize>,
2862        deps: &mut Vec<Dependency>,
2863        scope: &LetScope<'_>,
2864    ) -> Option<usize> {
2865        let mut upper = name.to_ascii_uppercase();
2866        if let Some(rest) = upper.strip_prefix("_XLFN.") {
2867            upper = rest.to_string();
2868        }
2869        if let Some(rest) = upper.strip_prefix("_XLWS.") {
2870            upper = rest.to_string();
2871        }
2872        match upper.as_str() {
2873            "TRANSPOSE" => {
2874                let (flat, cols) = self
2875                    .array_shape(args.first()?, context, row, col, deps, scope)
2876                    .ok()?;
2877                Some((flat.len().checked_div(cols).unwrap_or(0)).max(1))
2878            }
2879            "HSTACK" => {
2880                let mut total = 0usize;
2881                for a in args {
2882                    total += self.array_shape(a, context, row, col, deps, scope).ok()?.1;
2883                }
2884                Some(total)
2885            }
2886            "VSTACK" => {
2887                let mut max_cols = 0usize;
2888                for a in args {
2889                    max_cols =
2890                        max_cols.max(self.array_shape(a, context, row, col, deps, scope).ok()?.1);
2891                }
2892                Some(max_cols)
2893            }
2894            "CHOOSEROWS" => Some(
2895                self.array_shape(args.first()?, context, row, col, deps, scope)
2896                    .ok()?
2897                    .1,
2898            ),
2899            "CHOOSECOLS" => Some(args.len().saturating_sub(1).max(1)),
2900            "DROP" | "TAKE" => {
2901                let (_, cols) = self
2902                    .array_shape(args.first()?, context, row, col, deps, scope)
2903                    .ok()?;
2904                let is_take = upper == "TAKE";
2905                match args.get(2) {
2906                    Some(e) => {
2907                        let n = self
2908                            .to_f64(&self.evaluate_ast(e, context, row, col, deps, scope).ok()?)
2909                            .unwrap_or(0.0) as isize;
2910                        let (s, e2) = Self::drop_take_bounds(cols as isize, n, is_take);
2911                        Some((e2 - s).max(0) as usize)
2912                    }
2913                    None => Some(if is_take { cols } else { 0 }),
2914                }
2915            }
2916            "EXPAND" => {
2917                let (_, cols) = self
2918                    .array_shape(args.first()?, context, row, col, deps, scope)
2919                    .ok()?;
2920                match args.get(2) {
2921                    Some(e) => Some(
2922                        self.to_f64(&self.evaluate_ast(e, context, row, col, deps, scope).ok()?)
2923                            .unwrap_or(cols as f64) as usize,
2924                    ),
2925                    None => Some(cols),
2926                }
2927            }
2928            "TOCOL" => Some(1),
2929            "WRAPROWS" => {
2930                let n = self
2931                    .to_f64(
2932                        &self
2933                            .evaluate_ast(args.get(1)?, context, row, col, deps, scope)
2934                            .ok()?,
2935                    )
2936                    .unwrap_or(1.0)
2937                    .max(1.0) as usize;
2938                Some(n)
2939            }
2940            "WRAPCOLS" => {
2941                let (flat, _) = self
2942                    .array_shape(args.first()?, context, row, col, deps, scope)
2943                    .ok()?;
2944                let wrap = self
2945                    .to_f64(
2946                        &self
2947                            .evaluate_ast(args.get(1)?, context, row, col, deps, scope)
2948                            .ok()?,
2949                    )
2950                    .unwrap_or(1.0)
2951                    .max(1.0) as usize;
2952                Some(flat.len().div_ceil(wrap).max(1))
2953            }
2954            "UNIQUE" | "SORT" | "SORTBY" | "FILTER" | "TRIMRANGE" => Some(
2955                self.array_shape(args.first()?, context, row, col, deps, scope)
2956                    .ok()?
2957                    .1,
2958            ),
2959            "SEQUENCE" => match args.get(1) {
2960                Some(e) => Some(
2961                    self.to_f64(&self.evaluate_ast(e, context, row, col, deps, scope).ok()?)
2962                        .unwrap_or(1.0)
2963                        .max(1.0) as usize,
2964                ),
2965                None => Some(1),
2966            },
2967            "MUNIT" => {
2968                let n = self
2969                    .to_f64(
2970                        &self
2971                            .evaluate_ast(args.first()?, context, row, col, deps, scope)
2972                            .ok()?,
2973                    )
2974                    .unwrap_or(1.0)
2975                    .max(1.0) as usize;
2976                Some(n)
2977            }
2978            "MAKEARRAY" => match args.get(1) {
2979                Some(e) => Some(
2980                    self.to_f64(&self.evaluate_ast(e, context, row, col, deps, scope).ok()?)
2981                        .unwrap_or(1.0)
2982                        .max(1.0) as usize,
2983                ),
2984                None => Some(1),
2985            },
2986            _ => None,
2987        }
2988    }
2989
2990    /// Shared `[start, end)` bound computation for `TAKE`/`DROP`: a
2991    /// positive count counts from the start, negative from the end;
2992    /// `is_take` selects which side of that split is kept.
2993    fn drop_take_bounds(total: isize, n: isize, is_take: bool) -> (isize, isize) {
2994        let n = n.clamp(-total, total);
2995        if is_take {
2996            if n >= 0 { (0, n) } else { (total + n, total) }
2997        } else if n >= 0 {
2998            (n, total)
2999        } else {
3000            (0, total + n)
3001        }
3002    }
3003
3004    /// Shared implementation for MAP/BYROW/BYCOL/REDUCE/SCAN/MAKEARRAY:
3005    /// each applies a `LAMBDA` argument to some shape of input (parallel
3006    /// arrays, rows, columns, an accumulator, or generated row/col
3007    /// indices) and collects the results -- see each branch for the
3008    /// specific shape. Dynamic-array results are returned as a flat,
3009    /// row-major `ResultData::List`, the same convention `SEQUENCE`/
3010    /// `MUNIT`/etc. already use, since this engine doesn't spill formulas
3011    /// across cells; callers pull out a single value with `INDEX`.
3012    #[allow(clippy::too_many_arguments)]
3013    fn evaluate_lambda_function(
3014        &self,
3015        func_name: &str,
3016        args: &[crate::core::parser::Expr],
3017        context: Option<&Context>,
3018        row: Option<usize>,
3019        col: Option<usize>,
3020        deps: &mut Vec<Dependency>,
3021        scope: &LetScope<'_>,
3022    ) -> Result<ResultData, EngineError> {
3023        use crate::core::parser::Expr;
3024
3025        match func_name {
3026            "MAP" => {
3027                if args.len() < 2 {
3028                    return Ok(ResultData::Error("#VALUE!".to_string()));
3029                }
3030                let (lambda_expr, array_exprs) = args.split_last().unwrap();
3031                let Some((params, body)) = Self::extract_lambda(lambda_expr) else {
3032                    return Ok(ResultData::Error("#VALUE!".to_string()));
3033                };
3034                if params.len() != array_exprs.len() {
3035                    return Ok(ResultData::Error("#VALUE!".to_string()));
3036                }
3037                let arrays: Vec<Vec<ResultData>> = array_exprs
3038                    .iter()
3039                    .map(|e| self.eval_as_array(e, context, row, col, deps, scope))
3040                    .collect::<Result<_, _>>()?;
3041                let len = arrays.iter().map(|a| a.len()).max().unwrap_or(0);
3042                let mut results = Vec::with_capacity(len);
3043                for i in 0..len {
3044                    let values: Vec<ResultData> = arrays
3045                        .iter()
3046                        .map(|a| a.get(i).cloned().unwrap_or(ResultData::None))
3047                        .collect();
3048                    results.push(
3049                        self.invoke_lambda(&params, &values, body, context, row, col, deps, scope)?,
3050                    );
3051                }
3052                Ok(ResultData::List(results))
3053            }
3054            "BYROW" | "BYCOL" => {
3055                if args.len() != 2 {
3056                    return Ok(ResultData::Error("#VALUE!".to_string()));
3057                }
3058                let Some((params, body)) = Self::extract_lambda(&args[1]) else {
3059                    return Ok(ResultData::Error("#VALUE!".to_string()));
3060                };
3061                if params.len() != 1 {
3062                    return Ok(ResultData::Error("#VALUE!".to_string()));
3063                }
3064                // Recovers real column count the same way INDEX's 3-arg
3065                // form does: re-matching the raw AST node, since the
3066                // already-evaluated array argument is just a flat List.
3067                let num_cols = match &args[0] {
3068                    Expr::RangeRef {
3069                        start_col, end_col, ..
3070                    } => (end_col - start_col + 1).max(1),
3071                    _ => 1,
3072                };
3073                let flat = self.eval_as_array(&args[0], context, row, col, deps, scope)?;
3074                let num_rows = if num_cols == 0 {
3075                    0
3076                } else {
3077                    flat.len().div_ceil(num_cols)
3078                };
3079                let mut results = Vec::new();
3080                if func_name == "BYROW" {
3081                    for r in 0..num_rows {
3082                        let row_vals: Vec<ResultData> = (0..num_cols)
3083                            .filter_map(|c| flat.get(r * num_cols + c).cloned())
3084                            .collect();
3085                        let arg = vec![ResultData::List(row_vals)];
3086                        results.push(
3087                            self.invoke_lambda(
3088                                &params, &arg, body, context, row, col, deps, scope,
3089                            )?,
3090                        );
3091                    }
3092                } else {
3093                    for c in 0..num_cols {
3094                        let col_vals: Vec<ResultData> = (0..num_rows)
3095                            .filter_map(|r| flat.get(r * num_cols + c).cloned())
3096                            .collect();
3097                        let arg = vec![ResultData::List(col_vals)];
3098                        results.push(
3099                            self.invoke_lambda(
3100                                &params, &arg, body, context, row, col, deps, scope,
3101                            )?,
3102                        );
3103                    }
3104                }
3105                Ok(ResultData::List(results))
3106            }
3107            "REDUCE" | "SCAN" => {
3108                // initial_value is optional in real Excel's 3-argument
3109                // REDUCE/SCAN; since the parser has no dedicated "omitted
3110                // argument" syntax to express that, this implementation
3111                // also accepts a plain 2-argument call (array, lambda) as
3112                // the omitted-initial-value form, seeding the accumulator
3113                // from the array's own first element and folding over the
3114                // rest -- rather than only supporting a literal 3rd
3115                // argument that happens to error out.
3116                if args.len() != 2 && args.len() != 3 {
3117                    return Ok(ResultData::Error("#VALUE!".to_string()));
3118                }
3119                let lambda_idx = args.len() - 1;
3120                let array_idx = args.len() - 2;
3121                let Some((params, body)) = Self::extract_lambda(&args[lambda_idx]) else {
3122                    return Ok(ResultData::Error("#VALUE!".to_string()));
3123                };
3124                if params.len() != 2 {
3125                    return Ok(ResultData::Error("#VALUE!".to_string()));
3126                }
3127                let array = self.eval_as_array(&args[array_idx], context, row, col, deps, scope)?;
3128                // SCAN's output has the same length as `array` -- an
3129                // explicit initial_value (3-arg form) is external to the
3130                // array and doesn't get its own output entry (every entry
3131                // is a real fold), whereas the 2-arg fallback's seed *is*
3132                // the array's own first element, so it does.
3133                let (mut acc, rest, mut history): (ResultData, &[ResultData], Vec<ResultData>) =
3134                    if args.len() == 3 {
3135                        let init = self.evaluate_ast(&args[0], context, row, col, deps, scope)?;
3136                        (init, &array[..], Vec::new())
3137                    } else {
3138                        match array.split_first() {
3139                            Some((first, rest)) => (first.clone(), rest, vec![first.clone()]),
3140                            None => return Ok(ResultData::Error("#VALUE!".to_string())),
3141                        }
3142                    };
3143                for item in rest {
3144                    let call_args = [acc.clone(), item.clone()];
3145                    acc = self
3146                        .invoke_lambda(&params, &call_args, body, context, row, col, deps, scope)?;
3147                    history.push(acc.clone());
3148                }
3149                if func_name == "REDUCE" {
3150                    Ok(acc)
3151                } else {
3152                    Ok(ResultData::List(history))
3153                }
3154            }
3155            "MAKEARRAY" => {
3156                if args.len() != 3 {
3157                    return Ok(ResultData::Error("#VALUE!".to_string()));
3158                }
3159                let Some((params, body)) = Self::extract_lambda(&args[2]) else {
3160                    return Ok(ResultData::Error("#VALUE!".to_string()));
3161                };
3162                if params.len() != 2 {
3163                    return Ok(ResultData::Error("#VALUE!".to_string()));
3164                }
3165                let rows_val = self.evaluate_ast(&args[0], context, row, col, deps, scope)?;
3166                let cols_val = self.evaluate_ast(&args[1], context, row, col, deps, scope)?;
3167                let num_rows = self.to_f64(&rows_val).unwrap_or(0.0).max(0.0) as usize;
3168                let num_cols = self.to_f64(&cols_val).unwrap_or(0.0).max(0.0) as usize;
3169                let mut results = Vec::with_capacity(num_rows * num_cols);
3170                for r in 1..=num_rows {
3171                    for c in 1..=num_cols {
3172                        let call_args = [ResultData::Float(r as f64), ResultData::Float(c as f64)];
3173                        results.push(self.invoke_lambda(
3174                            &params, &call_args, body, context, row, col, deps, scope,
3175                        )?);
3176                    }
3177                }
3178                Ok(ResultData::List(results))
3179            }
3180            _ => unreachable!(),
3181        }
3182    }
3183
3184    /// Minimal A1-notation string parser for `INDIRECT`: `"A1"`,
3185    /// `"B2:C5"`, `"Sheet1!A1"`, `"Sheet1!A1:B2"`, with optional `$`
3186    /// absolute markers and an optional `'quoted sheet name'!` prefix.
3187    /// Deliberately small and local rather than shared with
3188    /// `visi/src/utils.rs`'s equivalent parser (`parse_cell_ref`/
3189    /// `parse_range_ref`): `visi-core` cannot depend on the `visi` crate
3190    /// (the dependency direction is the other way), so this necessarily
3191    /// duplicates that logic in miniature.
3192    fn parse_a1_reference(text: &str) -> Option<(Option<String>, usize, usize, usize, usize)> {
3193        let text = text.trim();
3194        let (sheet_part, ref_part) = match text.rfind('!') {
3195            Some(idx) => (Some(&text[..idx]), &text[idx + 1..]),
3196            None => (None, text),
3197        };
3198        let sheet = sheet_part.map(|s| s.trim().trim_matches('\'').to_string());
3199
3200        fn parse_cell(s: &str) -> Option<(usize, usize)> {
3201            let s = s.replace('$', "");
3202            let col_end = s.find(|c: char| c.is_ascii_digit())?;
3203            let (col_str, row_str) = s.split_at(col_end);
3204            if col_str.is_empty() || row_str.is_empty() {
3205                return None;
3206            }
3207            let mut col = 0usize;
3208            for ch in col_str.chars() {
3209                if !ch.is_ascii_alphabetic() {
3210                    return None;
3211                }
3212                col = col * 26 + (ch.to_ascii_uppercase() as usize - 'A' as usize + 1);
3213            }
3214            let row: usize = row_str.parse().ok()?;
3215            if row == 0 || col == 0 {
3216                return None;
3217            }
3218            Some((row - 1, col - 1))
3219        }
3220
3221        if let Some((start, end)) = ref_part.split_once(':') {
3222            let (r1, c1) = parse_cell(start)?;
3223            let (r2, c2) = parse_cell(end)?;
3224            Some((sheet, r1.min(r2), c1.min(c2), r1.max(r2), c1.max(c2)))
3225        } else {
3226            let (r, c) = parse_cell(ref_part)?;
3227            Some((sheet, r, c, r, c))
3228        }
3229    }
3230
3231    /// Reads a single cell, registering the appropriate local/remote
3232    /// dependency -- the same local-vs-remote branch used throughout this
3233    /// file (see e.g. `evaluate_ast`'s `Expr::CellRef` arm), factored out
3234    /// since `CELL`/`FORMULATEXT`/`ISFORMULA`/`INDIRECT`/`OFFSET` all need
3235    /// it for a reference resolved dynamically rather than parsed as an
3236    /// AST node.
3237    fn read_cell_with_deps(
3238        &self,
3239        sheet_opt: &Option<String>,
3240        r: usize,
3241        c: usize,
3242        context: Option<&Context>,
3243        deps: &mut Vec<Dependency>,
3244    ) -> ResultData {
3245        let is_self = sheet_opt.as_deref().is_none_or(|n| n == self.name);
3246        if is_self {
3247            deps.push(Dependency::Local(CellRef::new(r, c)));
3248            self.get_result_data(&CellRef::new(r, c))
3249        } else if let Some(ctx) = context {
3250            let name = sheet_opt.clone().unwrap();
3251            deps.push(Dependency::Remote {
3252                sheet: name.clone(),
3253                cell: CellRef::new(r, c),
3254            });
3255            ctx.sheets
3256                .get(&name)
3257                .map(|s| s.get_result_data(&CellRef::new(r, c)))
3258                .unwrap_or(ResultData::None)
3259        } else {
3260            ResultData::None
3261        }
3262    }
3263
3264    /// Shared implementation for the range/reference-introspection and
3265    /// workbook-metadata functions: ROW/ROWS/COLUMN/COLUMNS need the raw
3266    /// reference's real bounds (not a flattened `evaluated_args` value);
3267    /// AREAS/ISREF are purely syntactic checks on the argument's AST
3268    /// shape; FORMULATEXT/ISFORMULA need the cell's raw source text;
3269    /// INDIRECT/OFFSET build a reference dynamically instead of relying
3270    /// on one already resolved at parse time; SHEET/SHEETS/CELL/INFO
3271    /// report workbook/environment metadata.
3272    #[allow(clippy::too_many_arguments)]
3273    fn evaluate_range_info_function(
3274        &self,
3275        func_name: &str,
3276        args: &[crate::core::parser::Expr],
3277        context: Option<&Context>,
3278        row: Option<usize>,
3279        col: Option<usize>,
3280        deps: &mut Vec<Dependency>,
3281        scope: &LetScope<'_>,
3282    ) -> Result<ResultData, EngineError> {
3283        use crate::core::parser::Expr;
3284
3285        match func_name {
3286            "ROW" => match args.first() {
3287                // A multi-row reference returns an array of row numbers
3288                // (one per row spanned), not just the first one -- a
3289                // single-row reference (including a plain cell, where
3290                // start_row == end_row) still returns the plain scalar.
3291                Some(arg) => match Self::range_bounds(arg) {
3292                    Some((_, start_row, _, end_row, _)) if end_row > start_row => {
3293                        Ok(ResultData::List(
3294                            (start_row..=end_row)
3295                                .map(|r| ResultData::Float((r + 1) as f64))
3296                                .collect(),
3297                        ))
3298                    }
3299                    Some((_, start_row, _, _, _)) => Ok(ResultData::Float((start_row + 1) as f64)),
3300                    None => Ok(ResultData::Error("#VALUE!".to_string())),
3301                },
3302                None => match row {
3303                    Some(r) => Ok(ResultData::Float((r + 1) as f64)),
3304                    None => Ok(ResultData::Error("#VALUE!".to_string())),
3305                },
3306            },
3307            "COLUMN" => match args.first() {
3308                // Same array-vs-scalar distinction as ROW, but across
3309                // columns instead of rows.
3310                Some(arg) => match Self::range_bounds(arg) {
3311                    Some((_, _, start_col, _, end_col)) if end_col > start_col => {
3312                        Ok(ResultData::List(
3313                            (start_col..=end_col)
3314                                .map(|c| ResultData::Float((c + 1) as f64))
3315                                .collect(),
3316                        ))
3317                    }
3318                    Some((_, _, start_col, _, _)) => Ok(ResultData::Float((start_col + 1) as f64)),
3319                    None => Ok(ResultData::Error("#VALUE!".to_string())),
3320                },
3321                None => match col {
3322                    Some(c) => Ok(ResultData::Float((c + 1) as f64)),
3323                    None => Ok(ResultData::Error("#VALUE!".to_string())),
3324                },
3325            },
3326            "ROWS" => {
3327                let Some(arg) = args.first() else {
3328                    return Ok(ResultData::Error("#VALUE!".to_string()));
3329                };
3330                let Some((sheet_opt, start_row, _, end_row, _)) = Self::range_bounds(arg) else {
3331                    return Ok(ResultData::Error("#VALUE!".to_string()));
3332                };
3333                let is_self = sheet_opt.as_deref().is_none_or(|n| n == self.name);
3334                let actual_end_row = if end_row == usize::MAX {
3335                    if is_self {
3336                        self.row_count().saturating_sub(1)
3337                    } else {
3338                        context
3339                            .and_then(|ctx| sheet_opt.as_ref().and_then(|n| ctx.sheets.get(n)))
3340                            .map(|s| s.row_count().saturating_sub(1))
3341                            .unwrap_or(0)
3342                    }
3343                } else {
3344                    end_row
3345                };
3346                Ok(ResultData::Float(
3347                    (actual_end_row.saturating_sub(start_row) + 1) as f64,
3348                ))
3349            }
3350            "COLUMNS" => {
3351                let Some(arg) = args.first() else {
3352                    return Ok(ResultData::Error("#VALUE!".to_string()));
3353                };
3354                match Self::range_bounds(arg) {
3355                    Some((_, _, start_col, _, end_col)) => Ok(ResultData::Float(
3356                        (end_col.saturating_sub(start_col) + 1) as f64,
3357                    )),
3358                    None => Ok(ResultData::Error("#VALUE!".to_string())),
3359                }
3360            }
3361            "AREAS" => {
3362                // This engine's parser has no multi-area (comma-separated
3363                // union) reference syntax, so every reference is exactly
3364                // one area.
3365                if args.is_empty() {
3366                    Ok(ResultData::Error("#VALUE!".to_string()))
3367                } else {
3368                    Ok(ResultData::Float(1.0))
3369                }
3370            }
3371            "ISREF" => Ok(ResultData::Boolean(matches!(
3372                args.first(),
3373                Some(Expr::CellRef { .. } | Expr::RangeRef { .. } | Expr::StructuredRef { .. })
3374            ))),
3375            "FORMULATEXT" | "ISFORMULA" => {
3376                let Some(arg) = args.first() else {
3377                    return Ok(ResultData::Error("#VALUE!".to_string()));
3378                };
3379                let Some((sheet_opt, r, c, _, _)) = Self::range_bounds(arg) else {
3380                    return Ok(ResultData::Error("#VALUE!".to_string()));
3381                };
3382                let is_self = sheet_opt.as_deref().is_none_or(|n| n == self.name);
3383                let src = if is_self {
3384                    deps.push(Dependency::Local(CellRef::new(r, c)));
3385                    self.get_src_str(&CellRef::new(r, c))
3386                } else if let Some(ctx) = context {
3387                    let name = sheet_opt.unwrap();
3388                    deps.push(Dependency::Remote {
3389                        sheet: name.clone(),
3390                        cell: CellRef::new(r, c),
3391                    });
3392                    ctx.sheets
3393                        .get(&name)
3394                        .map(|s| s.get_src_str(&CellRef::new(r, c)))
3395                        .unwrap_or_default()
3396                } else {
3397                    String::new()
3398                };
3399                let is_formula = src.starts_with('=');
3400                if func_name == "ISFORMULA" {
3401                    Ok(ResultData::Boolean(is_formula))
3402                } else if is_formula {
3403                    Ok(ResultData::String(src))
3404                } else {
3405                    Ok(ResultData::Error("#N/A".to_string()))
3406                }
3407            }
3408            "SHEETS" => Ok(ResultData::Float(
3409                context.map(|c| c.sheets.len() + 1).unwrap_or(1) as f64,
3410            )),
3411            "SHEET" => {
3412                // With no argument, report this sheet's own ordinal. With
3413                // a reference argument, report the *referenced* sheet's
3414                // ordinal (a bare reference with no explicit sheet, e.g.
3415                // `SHEET(A1)`, means this sheet). Excel also accepts a
3416                // plain text sheet name, e.g. `SHEET("Sheet2")`.
3417                let sheet_name = match args.first() {
3418                    None => Some(self.name.clone()),
3419                    Some(arg) => match Self::range_bounds(arg) {
3420                        Some((sheet_opt, ..)) => {
3421                            Some(sheet_opt.unwrap_or_else(|| self.name.clone()))
3422                        }
3423                        None => self
3424                            .evaluate_ast(arg, context, row, col, deps, scope)
3425                            .ok()
3426                            .map(|v| v.to_string()),
3427                    },
3428                };
3429
3430                match sheet_name {
3431                    Some(name) => {
3432                        let ordinal = context
3433                            .and_then(|c| {
3434                                c.sheet_order
3435                                    .iter()
3436                                    .position(|n| n.eq_ignore_ascii_case(&name))
3437                            })
3438                            .map(|i| i + 1)
3439                            // No context (standalone eval outside a
3440                            // WorkbookManager pass) or the name wasn't
3441                            // found in workbook order: 1 is the same
3442                            // approximation this used unconditionally
3443                            // before.
3444                            .unwrap_or(1);
3445                        Ok(ResultData::Float(ordinal as f64))
3446                    }
3447                    None => Ok(ResultData::Error("#N/A".to_string())),
3448                }
3449            }
3450            "CELL" => {
3451                if args.is_empty() {
3452                    return Ok(ResultData::Error("#VALUE!".to_string()));
3453                }
3454                let info_type = self
3455                    .evaluate_ast(&args[0], context, row, col, deps, scope)?
3456                    .to_string()
3457                    .to_lowercase();
3458                let bounds = args.get(1).and_then(Self::range_bounds);
3459                match info_type.as_str() {
3460                    "row" => match bounds.map(|b| b.1).or(row) {
3461                        Some(r) => Ok(ResultData::Float((r + 1) as f64)),
3462                        None => Ok(ResultData::Error("#VALUE!".to_string())),
3463                    },
3464                    "col" => match bounds {
3465                        Some((_, _, c, _, _)) => Ok(ResultData::Float((c + 1) as f64)),
3466                        None => Ok(ResultData::Error("#VALUE!".to_string())),
3467                    },
3468                    "address" => match bounds {
3469                        Some((_, r, c, _, _)) => Ok(ResultData::String(format!(
3470                            "${}${}",
3471                            crate::core::parser::col_idx_to_letters(c),
3472                            r + 1
3473                        ))),
3474                        None => Ok(ResultData::Error("#VALUE!".to_string())),
3475                    },
3476                    "contents" => match bounds {
3477                        Some((sheet_opt, r, c, _, _)) => {
3478                            Ok(self.read_cell_with_deps(&sheet_opt, r, c, context, deps))
3479                        }
3480                        None => Ok(ResultData::Error("#VALUE!".to_string())),
3481                    },
3482                    _ => Ok(ResultData::Error("#VALUE!".to_string())),
3483                }
3484            }
3485            "INFO" => {
3486                if args.is_empty() {
3487                    return Ok(ResultData::Error("#VALUE!".to_string()));
3488                }
3489                let info_type = self
3490                    .evaluate_ast(&args[0], context, row, col, deps, scope)?
3491                    .to_string()
3492                    .to_lowercase();
3493                match info_type.as_str() {
3494                    "numfile" => Ok(ResultData::Float(
3495                        context.map(|c| c.sheets.len() + 1).unwrap_or(1) as f64,
3496                    )),
3497                    "release" => Ok(ResultData::String("16.0".to_string())),
3498                    "system" => Ok(ResultData::String(
3499                        if cfg!(target_os = "macos") {
3500                            "mac"
3501                        } else {
3502                            "pcdos"
3503                        }
3504                        .to_string(),
3505                    )),
3506                    _ => Ok(ResultData::Error("#VALUE!".to_string())),
3507                }
3508            }
3509            "INDIRECT" => {
3510                if args.is_empty() {
3511                    return Ok(ResultData::Error("#VALUE!".to_string()));
3512                }
3513                let text = self
3514                    .evaluate_ast(&args[0], context, row, col, deps, scope)?
3515                    .to_string();
3516                let a1_style = match args.get(1) {
3517                    Some(a) => self.to_bool(&self.evaluate_ast(a, context, row, col, deps, scope)?),
3518                    None => true,
3519                };
3520                if !a1_style {
3521                    // R1C1-style reference text isn't supported.
3522                    return Ok(ResultData::Error("#VALUE!".to_string()));
3523                }
3524                match Self::parse_a1_reference(&text) {
3525                    Some((sheet_opt, start_row, start_col, end_row, end_col)) => {
3526                        if start_row == end_row && start_col == end_col {
3527                            Ok(self.read_cell_with_deps(
3528                                &sheet_opt, start_row, start_col, context, deps,
3529                            ))
3530                        } else {
3531                            match self.materialize_range(
3532                                &sheet_opt, start_row, start_col, end_row, end_col, context,
3533                            ) {
3534                                Some(grid) => {
3535                                    Ok(ResultData::List(grid.into_iter().flatten().collect()))
3536                                }
3537                                None => Ok(ResultData::Error("#REF!".to_string())),
3538                            }
3539                        }
3540                    }
3541                    None => Ok(ResultData::Error("#REF!".to_string())),
3542                }
3543            }
3544            "OFFSET" => {
3545                if args.len() < 3 {
3546                    return Ok(ResultData::Error("#VALUE!".to_string()));
3547                }
3548                let Some((sheet_opt, base_row, base_col, base_end_row, base_end_col)) =
3549                    Self::range_bounds(&args[0])
3550                else {
3551                    return Ok(ResultData::Error("#VALUE!".to_string()));
3552                };
3553                let row_offset = self
3554                    .to_f64(&self.evaluate_ast(&args[1], context, row, col, deps, scope)?)
3555                    .unwrap_or(0.0) as isize;
3556                let col_offset = self
3557                    .to_f64(&self.evaluate_ast(&args[2], context, row, col, deps, scope)?)
3558                    .unwrap_or(0.0) as isize;
3559                let base_height = (base_end_row.saturating_sub(base_row) + 1) as isize;
3560                let base_width = (base_end_col.saturating_sub(base_col) + 1) as isize;
3561                let height = match args.get(3) {
3562                    Some(a) => self
3563                        .to_f64(&self.evaluate_ast(a, context, row, col, deps, scope)?)
3564                        .unwrap_or(base_height as f64) as isize,
3565                    None => base_height,
3566                };
3567                let width = match args.get(4) {
3568                    Some(a) => self
3569                        .to_f64(&self.evaluate_ast(a, context, row, col, deps, scope)?)
3570                        .unwrap_or(base_width as f64) as isize,
3571                    None => base_width,
3572                };
3573                let new_row = base_row as isize + row_offset;
3574                let new_col = base_col as isize + col_offset;
3575                if new_row < 0 || new_col < 0 || height <= 0 || width <= 0 {
3576                    return Ok(ResultData::Error("#REF!".to_string()));
3577                }
3578                let (start_row, start_col) = (new_row as usize, new_col as usize);
3579                let (end_row, end_col) = (
3580                    start_row + (height - 1) as usize,
3581                    start_col + (width - 1) as usize,
3582                );
3583                if start_row == end_row && start_col == end_col {
3584                    Ok(self.read_cell_with_deps(&sheet_opt, start_row, start_col, context, deps))
3585                } else {
3586                    match self.materialize_range(
3587                        &sheet_opt, start_row, start_col, end_row, end_col, context,
3588                    ) {
3589                        Some(grid) => Ok(ResultData::List(grid.into_iter().flatten().collect())),
3590                        None => Ok(ResultData::Error("#REF!".to_string())),
3591                    }
3592                }
3593            }
3594            _ => unreachable!(),
3595        }
3596    }
3597
3598    /// `GETPIVOTDATA(data_field, pivot_table_ref, [field, item]...)`.
3599    /// `pivot_table_ref` must stay an unevaluated cell reference (not a
3600    /// flattened value) so its sheet/row/col can be matched against
3601    /// `context.pivot_tables`' rendered destination ranges -- the same
3602    /// reason `ROW`/`OFFSET`/etc. go through `evaluate_range_info_function`
3603    /// instead of the generic eagerly-evaluated-args path below.
3604    fn evaluate_getpivotdata(
3605        &self,
3606        args: &[crate::core::parser::Expr],
3607        context: Option<&Context>,
3608        row: Option<usize>,
3609        col: Option<usize>,
3610        deps: &mut Vec<Dependency>,
3611        scope: &LetScope<'_>,
3612    ) -> Result<ResultData, EngineError> {
3613        if args.len() < 2 || !(args.len() - 2).is_multiple_of(2) {
3614            return Ok(ResultData::Error("#VALUE!".to_string()));
3615        }
3616
3617        let data_field = self
3618            .evaluate_ast(&args[0], context, row, col, deps, scope)?
3619            .to_string();
3620
3621        let (sheet_opt, target_row, target_col, _, _) = match Self::range_bounds(&args[1]) {
3622            Some(bounds) => bounds,
3623            None => return Ok(ResultData::Error("#REF!".to_string())),
3624        };
3625        // Registers the usual dependency on the referenced cell, mirroring
3626        // how INDIRECT/OFFSET treat a dynamically resolved reference.
3627        self.read_cell_with_deps(&sheet_opt, target_row, target_col, context, deps);
3628
3629        let sheet_id = match &sheet_opt {
3630            None => self.id,
3631            Some(name) if name == &self.name => self.id,
3632            Some(name) => match context.and_then(|c| c.sheets.get(name)) {
3633                Some(s) => s.id,
3634                None => return Ok(ResultData::Error("#REF!".to_string())),
3635            },
3636        };
3637
3638        let pivot_tables = context.map(|c| c.pivot_tables).unwrap_or(&[]);
3639        let pivot = match pivot_tables.iter().find(|p| {
3640            p.dest_sheet_id == sheet_id
3641                && p.last_output_end_row
3642                    .is_some_and(|end| target_row >= p.dest_row && target_row <= end)
3643                && p.last_output_end_col
3644                    .is_some_and(|end| target_col >= p.dest_col && target_col <= end)
3645        }) {
3646            Some(p) => p,
3647            None => return Ok(ResultData::Error("#REF!".to_string())),
3648        };
3649
3650        let mut criteria: Vec<(String, String)> = Vec::new();
3651        let mut i = 2;
3652        while i < args.len() {
3653            let field = self
3654                .evaluate_ast(&args[i], context, row, col, deps, scope)?
3655                .to_string();
3656            let item = self
3657                .evaluate_ast(&args[i + 1], context, row, col, deps, scope)?
3658                .to_string();
3659            criteria.push((field, item));
3660            i += 2;
3661        }
3662
3663        let mut sheet_refs: Vec<&Sheet> = context
3664            .map(|c| c.sheets.values().copied().collect())
3665            .unwrap_or_default();
3666        sheet_refs.push(self);
3667
3668        match crate::core::pivot::getpivotdata(&sheet_refs, pivot, &data_field, &criteria) {
3669            Ok(v) => Ok(v),
3670            Err(e) => Ok(ResultData::Error(e)),
3671        }
3672    }
3673
3674    /// Shared implementation for the dynamic-array reshaping functions.
3675    /// All operate on `array_shape`'s `(flat, num_cols)` view and return a
3676    /// flat, row-major `ResultData::List` -- the same convention
3677    /// `SEQUENCE`/`MUNIT`/`MAKEARRAY`/etc. already use, since this engine
3678    /// doesn't spill formulas across cells (a caller pulls out a single
3679    /// value with `INDEX`, or consumes the whole list with e.g. `SUM`).
3680    ///
3681    /// Known simplifications, each accepted given limited fuzzing time
3682    /// against real Excel for this batch: `UNIQUE`'s `by_col` and `SORT`'s
3683    /// `by_col` arguments are ignored (both always operate row-wise);
3684    /// `SORTBY` only supports a single `by_array`/`sort_order` pair, not
3685    /// the documented repeating list; `XMATCH`'s wildcard match mode and
3686    /// binary/reverse search modes aren't implemented (falls through to a
3687    /// forward linear scan).
3688    #[allow(clippy::too_many_arguments)]
3689    fn evaluate_array_reshape_function(
3690        &self,
3691        func_name: &str,
3692        args: &[crate::core::parser::Expr],
3693        context: Option<&Context>,
3694        row: Option<usize>,
3695        col: Option<usize>,
3696        deps: &mut Vec<Dependency>,
3697        scope: &LetScope<'_>,
3698    ) -> Result<ResultData, EngineError> {
3699        match func_name {
3700            "TRANSPOSE" => {
3701                let Some(arg) = args.first() else {
3702                    return Ok(ResultData::Error("#VALUE!".to_string()));
3703                };
3704                let (flat, cols) = self.array_shape(arg, context, row, col, deps, scope)?;
3705                let rows = flat.len().checked_div(cols).unwrap_or(0);
3706                let mut result = Vec::with_capacity(flat.len());
3707                for c in 0..cols {
3708                    for r in 0..rows {
3709                        result.push(flat[r * cols + c].clone());
3710                    }
3711                }
3712                Ok(ResultData::List(result))
3713            }
3714            "HSTACK" | "VSTACK" => {
3715                if args.is_empty() {
3716                    return Ok(ResultData::Error("#VALUE!".to_string()));
3717                }
3718                let mut shapes = Vec::with_capacity(args.len());
3719                for a in args {
3720                    shapes.push(self.array_shape(a, context, row, col, deps, scope)?);
3721                }
3722                let mut result = Vec::new();
3723                if func_name == "HSTACK" {
3724                    let max_rows = shapes
3725                        .iter()
3726                        .map(|(f, c)| if *c == 0 { 0 } else { f.len() / c })
3727                        .max()
3728                        .unwrap_or(0);
3729                    for r in 0..max_rows {
3730                        for (flat, cols) in &shapes {
3731                            let rows = if *cols == 0 { 0 } else { flat.len() / cols };
3732                            for c in 0..*cols {
3733                                result.push(if r < rows {
3734                                    flat[r * cols + c].clone()
3735                                } else {
3736                                    ResultData::Error("#N/A".to_string())
3737                                });
3738                            }
3739                        }
3740                    }
3741                } else {
3742                    let max_cols = shapes.iter().map(|(_, c)| *c).max().unwrap_or(0);
3743                    for (flat, cols) in &shapes {
3744                        let rows = if *cols == 0 { 0 } else { flat.len() / cols };
3745                        for r in 0..rows {
3746                            for c in 0..max_cols {
3747                                result.push(if c < *cols {
3748                                    flat[r * cols + c].clone()
3749                                } else {
3750                                    ResultData::Error("#N/A".to_string())
3751                                });
3752                            }
3753                        }
3754                    }
3755                }
3756                Ok(ResultData::List(result))
3757            }
3758            "CHOOSEROWS" | "CHOOSECOLS" => {
3759                if args.len() < 2 {
3760                    return Ok(ResultData::Error("#VALUE!".to_string()));
3761                }
3762                let (flat, cols) = self.array_shape(&args[0], context, row, col, deps, scope)?;
3763                let rows = flat.len().checked_div(cols).unwrap_or(0);
3764                let total = if func_name == "CHOOSEROWS" {
3765                    rows
3766                } else {
3767                    cols
3768                } as isize;
3769                let mut indices = Vec::with_capacity(args.len() - 1);
3770                for idx_expr in &args[1..] {
3771                    let n = self
3772                        .to_f64(&self.evaluate_ast(idx_expr, context, row, col, deps, scope)?)
3773                        .unwrap_or(0.0) as isize;
3774                    let real_idx = if n < 0 { total + n } else { n - 1 };
3775                    if real_idx < 0 || real_idx >= total {
3776                        return Ok(ResultData::Error("#VALUE!".to_string()));
3777                    }
3778                    indices.push(real_idx as usize);
3779                }
3780                let mut result = Vec::new();
3781                if func_name == "CHOOSEROWS" {
3782                    for r in indices {
3783                        for c in 0..cols {
3784                            result.push(flat[r * cols + c].clone());
3785                        }
3786                    }
3787                } else {
3788                    for r in 0..rows {
3789                        for &c in &indices {
3790                            result.push(flat[r * cols + c].clone());
3791                        }
3792                    }
3793                }
3794                Ok(ResultData::List(result))
3795            }
3796            "DROP" | "TAKE" => {
3797                if args.len() < 2 {
3798                    return Ok(ResultData::Error("#VALUE!".to_string()));
3799                }
3800                let (flat, cols) = self.array_shape(&args[0], context, row, col, deps, scope)?;
3801                let num_rows = flat.len().checked_div(cols).unwrap_or(0) as isize;
3802                let is_take = func_name == "TAKE";
3803                let rows_n = self
3804                    .to_f64(&self.evaluate_ast(&args[1], context, row, col, deps, scope)?)
3805                    .unwrap_or(0.0) as isize;
3806                let cols_n = match args.get(2) {
3807                    Some(e) => self
3808                        .to_f64(&self.evaluate_ast(e, context, row, col, deps, scope)?)
3809                        .unwrap_or(0.0) as isize,
3810                    None => {
3811                        if is_take {
3812                            cols as isize
3813                        } else {
3814                            0
3815                        }
3816                    }
3817                };
3818                let (row_start, row_end) = Self::drop_take_bounds(num_rows, rows_n, is_take);
3819                let (col_start, col_end) = Self::drop_take_bounds(cols as isize, cols_n, is_take);
3820                if row_start >= row_end || col_start >= col_end {
3821                    return Ok(ResultData::Error("#CALC!".to_string()));
3822                }
3823                let mut result = Vec::new();
3824                for r in row_start..row_end {
3825                    for c in col_start..col_end {
3826                        result.push(flat[(r as usize) * cols + (c as usize)].clone());
3827                    }
3828                }
3829                Ok(ResultData::List(result))
3830            }
3831            "EXPAND" => {
3832                if args.len() < 2 {
3833                    return Ok(ResultData::Error("#VALUE!".to_string()));
3834                }
3835                let (flat, cols) = self.array_shape(&args[0], context, row, col, deps, scope)?;
3836                let orig_rows = flat.len().checked_div(cols).unwrap_or(0);
3837                let new_rows = self
3838                    .to_f64(&self.evaluate_ast(&args[1], context, row, col, deps, scope)?)
3839                    .unwrap_or(orig_rows as f64) as usize;
3840                let new_cols = match args.get(2) {
3841                    Some(e) => self
3842                        .to_f64(&self.evaluate_ast(e, context, row, col, deps, scope)?)
3843                        .unwrap_or(cols as f64) as usize,
3844                    None => cols,
3845                };
3846                let pad = match args.get(3) {
3847                    Some(e) => self.evaluate_ast(e, context, row, col, deps, scope)?,
3848                    None => ResultData::Error("#N/A".to_string()),
3849                };
3850                if new_rows < orig_rows || new_cols < cols {
3851                    return Ok(ResultData::Error("#VALUE!".to_string()));
3852                }
3853                let mut result = Vec::with_capacity(new_rows * new_cols);
3854                for r in 0..new_rows {
3855                    for c in 0..new_cols {
3856                        result.push(if r < orig_rows && c < cols {
3857                            flat[r * cols + c].clone()
3858                        } else {
3859                            pad.clone()
3860                        });
3861                    }
3862                }
3863                Ok(ResultData::List(result))
3864            }
3865            "TOCOL" | "TOROW" => {
3866                let Some(arg) = args.first() else {
3867                    return Ok(ResultData::Error("#VALUE!".to_string()));
3868                };
3869                let (flat, cols) = self.array_shape(arg, context, row, col, deps, scope)?;
3870                let rows = flat.len().checked_div(cols).unwrap_or(0);
3871                let ignore = match args.get(1) {
3872                    Some(e) => self
3873                        .to_f64(&self.evaluate_ast(e, context, row, col, deps, scope)?)
3874                        .unwrap_or(0.0) as i64,
3875                    None => 0,
3876                };
3877                let scan_by_col = match args.get(2) {
3878                    Some(e) => self.to_bool(&self.evaluate_ast(e, context, row, col, deps, scope)?),
3879                    None => false,
3880                };
3881                let ordered: Vec<ResultData> = if scan_by_col {
3882                    let mut v = Vec::with_capacity(flat.len());
3883                    for c in 0..cols {
3884                        for r in 0..rows {
3885                            v.push(flat[r * cols + c].clone());
3886                        }
3887                    }
3888                    v
3889                } else {
3890                    flat
3891                };
3892                let filtered: Vec<ResultData> = ordered
3893                    .into_iter()
3894                    .filter(|v| match ignore {
3895                        1 => !matches!(v, ResultData::None),
3896                        2 => !matches!(v, ResultData::Error(_)),
3897                        3 => !matches!(v, ResultData::None | ResultData::Error(_)),
3898                        _ => true,
3899                    })
3900                    .collect();
3901                Ok(ResultData::List(filtered))
3902            }
3903            "WRAPROWS" | "WRAPCOLS" => {
3904                if args.len() < 2 {
3905                    return Ok(ResultData::Error("#VALUE!".to_string()));
3906                }
3907                let (flat, _cols) = self.array_shape(&args[0], context, row, col, deps, scope)?;
3908                let wrap = self
3909                    .to_f64(&self.evaluate_ast(&args[1], context, row, col, deps, scope)?)
3910                    .unwrap_or(1.0)
3911                    .max(1.0) as usize;
3912                let pad = match args.get(2) {
3913                    Some(e) => self.evaluate_ast(e, context, row, col, deps, scope)?,
3914                    None => ResultData::Error("#N/A".to_string()),
3915                };
3916                if func_name == "WRAPROWS" {
3917                    // Row-major flat storage with num_cols == wrap is
3918                    // exactly the padded input sequence itself.
3919                    let mut result = flat;
3920                    let rem = result.len() % wrap;
3921                    if rem != 0 {
3922                        result.extend(std::iter::repeat_n(pad, wrap - rem));
3923                    }
3924                    Ok(ResultData::List(result))
3925                } else {
3926                    let num_result_cols = flat.len().div_ceil(wrap).max(1);
3927                    let total = wrap * num_result_cols;
3928                    let mut result = Vec::with_capacity(total);
3929                    for i in 0..total {
3930                        let col = i / wrap;
3931                        let r = i % wrap;
3932                        let target = r * num_result_cols + col;
3933                        while result.len() <= target {
3934                            result.push(pad.clone());
3935                        }
3936                        if i < flat.len() {
3937                            result[target] = flat[i].clone();
3938                        }
3939                    }
3940                    Ok(ResultData::List(result))
3941                }
3942            }
3943            "UNIQUE" => {
3944                let Some(arg) = args.first() else {
3945                    return Ok(ResultData::Error("#VALUE!".to_string()));
3946                };
3947                let (flat, _cols) = self.array_shape(arg, context, row, col, deps, scope)?;
3948                let exactly_once = match args.get(2) {
3949                    Some(e) => self.to_bool(&self.evaluate_ast(e, context, row, col, deps, scope)?),
3950                    None => false,
3951                };
3952                let mut seen: Vec<(String, ResultData, usize)> = Vec::new();
3953                for v in &flat {
3954                    // UNIQUE compares values without the cross-type coercion
3955                    // used by worksheet comparison operators: text "3" and
3956                    // numeric 3 are distinct values.
3957                    let key = match v {
3958                        ResultData::None => "blank:".to_string(),
3959                        ResultData::Boolean(b) => format!("bool:{b}"),
3960                        ResultData::Integer(i) => format!("num:{}", *i as f64),
3961                        ResultData::Float(f) => format!("num:{f}"),
3962                        ResultData::String(s) => format!("str:{s}"),
3963                        ResultData::Error(e) => format!("err:{e}"),
3964                        ResultData::List(_) | ResultData::Dict(_) => format!("other:{v}"),
3965                    };
3966                    match seen.iter_mut().find(|(k, ..)| k == &key) {
3967                        Some(entry) => entry.2 += 1,
3968                        None => seen.push((key, v.clone(), 1)),
3969                    }
3970                }
3971                let result: Vec<ResultData> = seen
3972                    .into_iter()
3973                    .filter(|(_, _, count)| !exactly_once || *count == 1)
3974                    .map(|(_, v, _)| v)
3975                    .collect();
3976                Ok(ResultData::List(result))
3977            }
3978            "SORT" => {
3979                let Some(arg) = args.first() else {
3980                    return Ok(ResultData::Error("#VALUE!".to_string()));
3981                };
3982                let (flat, cols) = self.array_shape(arg, context, row, col, deps, scope)?;
3983                let rows = flat.len().checked_div(cols).unwrap_or(0);
3984                let sort_index = match args.get(1) {
3985                    Some(e) => self
3986                        .to_f64(&self.evaluate_ast(e, context, row, col, deps, scope)?)
3987                        .unwrap_or(1.0) as usize,
3988                    None => 1,
3989                };
3990                let sort_order = match args.get(2) {
3991                    Some(e) => self
3992                        .to_f64(&self.evaluate_ast(e, context, row, col, deps, scope)?)
3993                        .unwrap_or(1.0),
3994                    None => 1.0,
3995                };
3996                let col_idx = sort_index.saturating_sub(1).min(cols.saturating_sub(1));
3997                let mut row_indices: Vec<usize> = (0..rows).collect();
3998                row_indices.sort_by(|&a, &b| {
3999                    Self::sort_compare_blanks_last(
4000                        &flat[a * cols + col_idx],
4001                        &flat[b * cols + col_idx],
4002                        sort_order,
4003                    )
4004                });
4005                let mut result = Vec::with_capacity(flat.len());
4006                for r in row_indices {
4007                    for c in 0..cols {
4008                        result.push(flat[r * cols + c].clone());
4009                    }
4010                }
4011                Ok(ResultData::List(result))
4012            }
4013            "SORTBY" => {
4014                if args.len() < 2 {
4015                    return Ok(ResultData::Error("#VALUE!".to_string()));
4016                }
4017                let (flat, cols) = self.array_shape(&args[0], context, row, col, deps, scope)?;
4018                let rows = flat.len().checked_div(cols).unwrap_or(0);
4019                let by = self.eval_as_array(&args[1], context, row, col, deps, scope)?;
4020                let order = match args.get(2) {
4021                    Some(e) => self
4022                        .to_f64(&self.evaluate_ast(e, context, row, col, deps, scope)?)
4023                        .unwrap_or(1.0),
4024                    None => 1.0,
4025                };
4026                let mut row_indices: Vec<usize> = (0..rows).collect();
4027                row_indices.sort_by(|&a, &b| {
4028                    let va = by.get(a).cloned().unwrap_or(ResultData::None);
4029                    let vb = by.get(b).cloned().unwrap_or(ResultData::None);
4030                    Self::sort_compare_blanks_last(&va, &vb, order)
4031                });
4032                let mut result = Vec::with_capacity(flat.len());
4033                for r in row_indices {
4034                    for c in 0..cols {
4035                        result.push(flat[r * cols + c].clone());
4036                    }
4037                }
4038                Ok(ResultData::List(result))
4039            }
4040            "FILTER" => {
4041                if args.len() < 2 {
4042                    return Ok(ResultData::Error("#VALUE!".to_string()));
4043                }
4044                let (flat, cols) = self.array_shape(&args[0], context, row, col, deps, scope)?;
4045                let rows = flat.len().checked_div(cols).unwrap_or(0);
4046                let include = self.eval_as_array(&args[1], context, row, col, deps, scope)?;
4047                let mut result = Vec::new();
4048                for r in 0..rows {
4049                    let keep = include.get(r).map(|v| self.to_bool(v)).unwrap_or(false);
4050                    if keep {
4051                        for c in 0..cols {
4052                            result.push(flat[r * cols + c].clone());
4053                        }
4054                    }
4055                }
4056                if result.is_empty() {
4057                    match args.get(2) {
4058                        Some(e) => Ok(self.evaluate_ast(e, context, row, col, deps, scope)?),
4059                        None => Ok(ResultData::Error("#CALC!".to_string())),
4060                    }
4061                } else {
4062                    Ok(ResultData::List(result))
4063                }
4064            }
4065            "TRIMRANGE" => {
4066                let Some(arg) = args.first() else {
4067                    return Ok(ResultData::Error("#VALUE!".to_string()));
4068                };
4069                let (flat, cols) = self.array_shape(arg, context, row, col, deps, scope)?;
4070                let rows = flat.len().checked_div(cols).unwrap_or(0);
4071                let is_blank = |v: &ResultData| {
4072                    matches!(v, ResultData::None)
4073                        || matches!(v, ResultData::String(s) if s.is_empty())
4074                };
4075                let row_blank = |r: usize| (0..cols).all(|c| is_blank(&flat[r * cols + c]));
4076                let col_blank = |c: usize| (0..rows).all(|r| is_blank(&flat[r * cols + c]));
4077                let mut r_start = 0;
4078                while r_start < rows && row_blank(r_start) {
4079                    r_start += 1;
4080                }
4081                let mut r_end = rows;
4082                while r_end > r_start && row_blank(r_end - 1) {
4083                    r_end -= 1;
4084                }
4085                let mut c_start = 0;
4086                while c_start < cols && col_blank(c_start) {
4087                    c_start += 1;
4088                }
4089                let mut c_end = cols;
4090                while c_end > c_start && col_blank(c_end - 1) {
4091                    c_end -= 1;
4092                }
4093                let mut result = Vec::new();
4094                for r in r_start..r_end {
4095                    for c in c_start..c_end {
4096                        result.push(flat[r * cols + c].clone());
4097                    }
4098                }
4099                Ok(ResultData::List(result))
4100            }
4101            _ => unreachable!(),
4102        }
4103    }
4104
4105    /// The raw text typed into a cell -- `"10"`, `"=SUM(A1:A2)"` -- or `None`
4106    /// if the cell is outside the sheet's allocated grid.
4107    ///
4108    /// This is the input, not the result; see [`Sheet::get_result_data`] for
4109    /// the computed value and `Sheet::get_display_string` for what a user
4110    /// should see.
4111    pub fn get_src(&self, cell: &CellRef) -> Option<&String> {
4112        let col = self.columns.get(cell.col);
4113        if let Some(col) = col {
4114            col.src.get(cell.row)
4115        } else {
4116            None
4117        }
4118    }
4119
4120    /// [`Sheet::get_src`] with an out-of-range cell flattened to an owned
4121    /// empty string.
4122    pub fn get_src_str(&self, cell: &CellRef) -> String {
4123        let col = self.columns.get(cell.col);
4124        if let Some(col) = col {
4125            col.src.get(cell.row).cloned().unwrap_or("".to_string())
4126        } else {
4127            "".to_string()
4128        }
4129    }
4130
4131    /// [`Sheet::get_src`] as a borrowed `&str`, for callers that only read.
4132    pub fn get_src_str_ref(&self, cell: &CellRef) -> Option<&str> {
4133        let col = self.columns.get(cell.col)?;
4134        col.src.get(cell.row).map(|s| s.as_str())
4135    }
4136
4137    /// The word surrounding `char_offset` in a cell's source text, as a
4138    /// half-open range of character (not byte) indices -- what an editor needs
4139    /// for word-wise selection. See [`get_word_boundaries_from_str`].
4140    pub fn get_word_boundaries(&self, cell: &CellRef, char_offset: usize) -> (usize, usize) {
4141        let text = self.get_src_str(cell);
4142        get_word_boundaries_from_str(&text, char_offset)
4143    }
4144}