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