Skip to main content

visi_core/core/engine/sheet/
mod.rs

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