Skip to main content

visi_core/core/engine/sheet/
mod.rs

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