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