Skip to main content

visi_core/core/engine/sheet/
mod.rs

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