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