formualizer_eval/
test_workbook.rs

1//! crates/formualizer-eval/src/test_workbook.rs
2//! --------------------------------------------
3//! Lightweight in-memory workbook for unit/prop tests.
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use crate::engine::range_view::RangeView;
8use crate::function::Function;
9use crate::traits::{
10    EvaluationContext, FunctionProvider, NamedRangeResolver, Range, RangeResolver,
11    ReferenceResolver, Resolver, Table, TableResolver,
12};
13use formualizer_common::{ExcelError, LiteralValue, parse_a1_1based};
14use formualizer_parse::{
15    ExcelErrorKind,
16    parser::{ReferenceType, TableReference},
17};
18
19type V = LiteralValue;
20type CellKey = (u32, u32); // 1-based (row, col)
21
22#[derive(Default, Clone)]
23struct Sheet {
24    cells: HashMap<CellKey, V>,
25}
26
27#[derive(Default)]
28pub struct TestWorkbook {
29    sheets: HashMap<String, Sheet>,
30    named: HashMap<String, Vec<Vec<V>>>,
31    tables: HashMap<String, Box<dyn Table>>,
32    fns: HashMap<(String, String), Arc<dyn Function>>,
33    aliases: HashMap<(String, String), (String, String)>,
34}
35
36impl TestWorkbook {
37    /* ─────────────── constructors ─────────────── */
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    fn default_sheet_name(&self) -> &str {
43        const FALLBACK: &str = "Sheet1";
44        if self.sheets.contains_key(FALLBACK) {
45            return FALLBACK;
46        }
47        self.sheets
48            .keys()
49            .min()
50            .map(|s| s.as_str())
51            .unwrap_or(FALLBACK)
52    }
53
54    /* ─────────────── cell helpers ─────────────── */
55    pub fn with_cell<S: Into<String>>(mut self, sheet: S, row: u32, col: u32, v: V) -> Self {
56        let sh = self.sheets.entry(sheet.into()).or_default();
57        sh.cells.insert((row, col), v);
58        self
59    }
60
61    pub fn with_cell_a1<S: Into<String>, A: AsRef<str>>(self, sheet: S, a1: A, v: V) -> Self {
62        let (col, row) = parse_a1(a1.as_ref()).expect("bad A1 ref in with_cell_a1");
63        self.with_cell(sheet, row, col, v)
64    }
65
66    pub fn with_range<S: Into<String>>(
67        mut self,
68        sheet: S,
69        row: u32,
70        col: u32,
71        data: Vec<Vec<V>>,
72    ) -> Self {
73        let sh = self.sheets.entry(sheet.into()).or_default();
74        for (r_off, r) in data.into_iter().enumerate() {
75            for (c_off, v) in r.into_iter().enumerate() {
76                sh.cells.insert((row + r_off as u32, col + c_off as u32), v);
77            }
78        }
79        self
80    }
81
82    /* ─────────────── named ranges ─────────────── */
83    pub fn with_named_range<S: Into<String>>(mut self, name: S, data: Vec<Vec<V>>) -> Self {
84        self.named.insert(name.into(), data);
85        self
86    }
87
88    /* ─────────────── tables (placeholder) ─────── */
89    pub fn with_table<T: Table + 'static, S: Into<String>>(mut self, name: S, table: T) -> Self {
90        self.tables.insert(name.into(), Box::new(table));
91        self
92    }
93
94    /// Create a simple in-memory table with headers, optional totals, and data.
95    /// headers: column names in order; data: Vec of rows; totals: optional row of totals matching width
96    pub fn with_simple_table<S: Into<String>>(
97        mut self,
98        name: S,
99        headers: Vec<String>,
100        data: Vec<Vec<V>>,
101        totals: Option<Vec<V>>,
102    ) -> Self {
103        #[derive(Debug)]
104        struct SimpleTable {
105            headers: Vec<String>,
106            data: Vec<Vec<V>>, // HxW
107            totals: Option<Vec<V>>,
108        }
109        impl Table for SimpleTable {
110            fn get_cell(&self, row: usize, column: &str) -> Result<V, ExcelError> {
111                // row is 0-based within data body
112                let col_idx = self
113                    .headers
114                    .iter()
115                    .position(|h| h.eq_ignore_ascii_case(column))
116                    .ok_or_else(|| ExcelError::from(ExcelErrorKind::Ref))?;
117                self.data
118                    .get(row)
119                    .and_then(|r| r.get(col_idx))
120                    .cloned()
121                    .ok_or_else(|| ExcelError::from(ExcelErrorKind::Ref))
122            }
123            fn get_column(&self, column: &str) -> Result<Box<dyn Range>, ExcelError> {
124                let col_idx = self
125                    .headers
126                    .iter()
127                    .position(|h| h.eq_ignore_ascii_case(column))
128                    .ok_or_else(|| ExcelError::from(ExcelErrorKind::Ref))?;
129                let mut col: Vec<Vec<V>> = Vec::with_capacity(self.data.len());
130                for r in &self.data {
131                    col.push(vec![r.get(col_idx).cloned().unwrap_or(V::Empty)]);
132                }
133                Ok(Box::new(crate::traits::InMemoryRange::new(col)))
134            }
135            fn columns(&self) -> Vec<String> {
136                self.headers.clone()
137            }
138            fn data_height(&self) -> usize {
139                self.data.len()
140            }
141            fn has_headers(&self) -> bool {
142                true
143            }
144            fn has_totals(&self) -> bool {
145                self.totals.is_some()
146            }
147            fn headers_row(&self) -> Option<Box<dyn Range>> {
148                Some(Box::new(crate::traits::InMemoryRange::new(vec![
149                    self.headers
150                        .iter()
151                        .cloned()
152                        .map(LiteralValue::Text)
153                        .collect(),
154                ])))
155            }
156            fn totals_row(&self) -> Option<Box<dyn Range>> {
157                self.totals.as_ref().map(|t| {
158                    Box::new(crate::traits::InMemoryRange::new(vec![t.clone()])) as Box<dyn Range>
159                })
160            }
161            fn data_body(&self) -> Option<Box<dyn Range>> {
162                Some(Box::new(crate::traits::InMemoryRange::new(
163                    self.data.clone(),
164                )))
165            }
166            fn clone_box(&self) -> Box<dyn Table> {
167                Box::new(SimpleTable {
168                    headers: self.headers.clone(),
169                    data: self.data.clone(),
170                    totals: self.totals.clone(),
171                })
172            }
173        }
174        let table = SimpleTable {
175            headers,
176            data,
177            totals,
178        };
179        self.tables.insert(name.into(), Box::new(table));
180        self
181    }
182
183    /* ─────────────── function helpers ─────────── */
184    pub fn with_function(mut self, func: Arc<dyn Function>) -> Self {
185        let ns = func.namespace().to_uppercase();
186        let name = func.name().to_uppercase();
187        for &alias in func.aliases() {
188            if !alias.eq_ignore_ascii_case(&name) {
189                // store alias mapping in workbook-scoped alias map
190                let akey = (ns.clone(), alias.to_uppercase());
191                self.aliases.insert(akey, (ns.clone(), name.clone()));
192            }
193        }
194        self.fns.insert((ns, name), func);
195        self
196    }
197
198    /// Register an alias for a function in this workbook (test helper)
199    pub fn with_alias<S: AsRef<str>>(
200        mut self,
201        ns: S,
202        alias: S,
203        target_ns: S,
204        target_name: S,
205    ) -> Self {
206        let key = (ns.as_ref().to_uppercase(), alias.as_ref().to_uppercase());
207        let val = (
208            target_ns.as_ref().to_uppercase(),
209            target_name.as_ref().to_uppercase(),
210        );
211        self.aliases.insert(key, val);
212        self
213    }
214
215    /* ─────────────── interpreter shortcut ─────── */
216    pub fn interpreter(&self) -> crate::interpreter::Interpreter<'_> {
217        crate::interpreter::Interpreter::new(self, self.default_sheet_name())
218    }
219}
220
221/* ─────────────────────── trait impls ─────────────────────── */
222impl EvaluationContext for TestWorkbook {
223    fn resolve_range_view<'c>(
224        &'c self,
225        reference: &ReferenceType,
226        current_sheet: &str,
227    ) -> Result<RangeView<'c>, ExcelError> {
228        use formualizer_parse::parser::ReferenceType as RT;
229
230        fn qualify_reference(reference: &RT, current_sheet: &str) -> RT {
231            match reference {
232                RT::Cell {
233                    sheet: None,
234                    row,
235                    col,
236                } => RT::Cell {
237                    sheet: Some(current_sheet.to_string()),
238                    row: *row,
239                    col: *col,
240                },
241                RT::Range {
242                    sheet: None,
243                    start_row,
244                    start_col,
245                    end_row,
246                    end_col,
247                } => RT::Range {
248                    sheet: Some(current_sheet.to_string()),
249                    start_row: *start_row,
250                    start_col: *start_col,
251                    end_row: *end_row,
252                    end_col: *end_col,
253                },
254                _ => reference.clone(),
255            }
256        }
257
258        match reference {
259            // Preserve #REF! for invalid single-cell references by embedding as a 1x1 value
260            RT::Cell { sheet, row, col } => {
261                let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
262                let v = match self.resolve_cell_reference(Some(sheet_name), *row, *col) {
263                    Ok(val) => val,
264                    Err(e) => V::Error(e),
265                };
266                let owned = vec![vec![v]];
267                Ok(RangeView::from_borrowed(Box::leak(Box::new(owned))))
268            }
269            // Named range: delegate to resolver so missing names become #NAME?
270            RT::NamedRange(name) => {
271                let rows = self.resolve_named_range_reference(name)?;
272                Ok(RangeView::from_borrowed(Box::leak(Box::new(rows))))
273            }
274            // Tables and rectangular ranges: materialize via generic path
275            _ => {
276                let qualified = qualify_reference(reference, current_sheet);
277                let range_box = self.resolve_range_like(&qualified)?;
278                let owned: Vec<Vec<V>> = range_box.materialise().into_owned();
279                Ok(RangeView::from_borrowed(Box::leak(Box::new(owned))))
280            }
281        }
282    }
283
284    fn used_rows_for_columns(
285        &self,
286        sheet: &str,
287        start_col: u32,
288        end_col: u32,
289    ) -> Option<(u32, u32)> {
290        let sh = self.sheets.get(sheet)?;
291        let mut min_r: Option<u32> = None;
292        let mut max_r: Option<u32> = None;
293        for &(r, c) in sh.cells.keys() {
294            if c >= start_col && c <= end_col {
295                min_r = Some(min_r.map(|m| m.min(r)).unwrap_or(r));
296                max_r = Some(max_r.map(|m| m.max(r)).unwrap_or(r));
297            }
298        }
299        match (min_r, max_r) {
300            (Some(a), Some(b)) => Some((a, b)),
301            _ => None,
302        }
303    }
304
305    fn used_cols_for_rows(&self, sheet: &str, start_row: u32, end_row: u32) -> Option<(u32, u32)> {
306        let sh = self.sheets.get(sheet)?;
307        let mut min_c: Option<u32> = None;
308        let mut max_c: Option<u32> = None;
309        for &(r, c) in sh.cells.keys() {
310            if r >= start_row && r <= end_row {
311                min_c = Some(min_c.map(|m| m.min(c)).unwrap_or(c));
312                max_c = Some(max_c.map(|m| m.max(c)).unwrap_or(c));
313            }
314        }
315        match (min_c, max_c) {
316            (Some(a), Some(b)) => Some((a, b)),
317            _ => None,
318        }
319    }
320
321    fn sheet_bounds(&self, _sheet: &str) -> Option<(u32, u32)> {
322        Some((1_048_576, 16_384))
323    }
324
325    fn backend_caps(&self) -> crate::traits::BackendCaps {
326        crate::traits::BackendCaps {
327            streaming: false,
328            used_region: true,
329            write: false,
330            tables: false,
331            async_stream: false,
332        }
333    }
334}
335impl ReferenceResolver for TestWorkbook {
336    fn resolve_cell_reference(
337        &self,
338        sheet: Option<&str>,
339        row: u32,
340        col: u32,
341    ) -> Result<V, ExcelError> {
342        let sheet_name = sheet.unwrap_or(self.default_sheet_name());
343        self.sheets
344            .get(sheet_name)
345            .and_then(|sh| sh.cells.get(&(row, col)).cloned())
346            .ok_or_else(|| ExcelError::from(ExcelErrorKind::Ref))
347    }
348}
349
350impl RangeResolver for TestWorkbook {
351    fn resolve_range_reference(
352        &self,
353        sheet: Option<&str>,
354        sr: Option<u32>,
355        sc: Option<u32>,
356        er: Option<u32>,
357        ec: Option<u32>,
358    ) -> Result<Box<dyn Range>, ExcelError> {
359        let (sr, sc, er, ec) = match (sr, sc, er, ec) {
360            (Some(sr), Some(sc), Some(er), Some(ec)) => (sr, sc, er, ec),
361            _ => return Err(ExcelError::from(ExcelErrorKind::NImpl)),
362        };
363        let sheet_name = sheet.unwrap_or(self.default_sheet_name());
364        let sh = self
365            .sheets
366            .get(sheet_name)
367            .ok_or_else(|| ExcelError::from(ExcelErrorKind::Ref))?;
368        let mut data = Vec::with_capacity((er - sr + 1) as usize);
369        for r in sr..=er {
370            let mut row_vec = Vec::with_capacity((ec - sc + 1) as usize);
371            for c in sc..=ec {
372                row_vec.push(sh.cells.get(&(r, c)).cloned().unwrap_or(V::Empty));
373            }
374            data.push(row_vec);
375        }
376        Ok(Box::new(crate::traits::InMemoryRange::new(data)))
377    }
378}
379
380impl NamedRangeResolver for TestWorkbook {
381    fn resolve_named_range_reference(&self, name: &str) -> Result<Vec<Vec<V>>, ExcelError> {
382        self.named
383            .get(name)
384            .cloned()
385            .ok_or_else(|| ExcelError::from(ExcelErrorKind::Name))
386    }
387}
388
389impl TableResolver for TestWorkbook {
390    fn resolve_table_reference(&self, tref: &TableReference) -> Result<Box<dyn Table>, ExcelError> {
391        self.tables
392            .get(&tref.name)
393            .map(|table_box| table_box.as_ref().clone_box())
394            .ok_or_else(|| ExcelError::from(ExcelErrorKind::NImpl))
395    }
396}
397
398impl FunctionProvider for TestWorkbook {
399    fn get_function(&self, ns: &str, name: &str) -> Option<Arc<dyn Function>> {
400        let nns = ns.to_uppercase();
401        let nname = name.to_uppercase();
402        // direct hit
403        if let Some(f) = self.fns.get(&(nns.clone(), nname.clone())) {
404            return Some(f.clone());
405        }
406        // alias in workbook scope
407        if let Some((t_ns, t_name)) = self.aliases.get(&(nns.clone(), nname.clone()))
408            && let Some(f) = self.fns.get(&(t_ns.clone(), t_name.clone()))
409        {
410            return Some(f.clone());
411        }
412        // fall back to global registry (case-insensitive with aliases)
413        crate::function_registry::get(&nns, &nname)
414    }
415}
416
417/* blanket */
418impl Resolver for TestWorkbook {}
419
420/* ─────────────────────── A1 parser ───────────────────────── */
421fn parse_a1(a1: &str) -> Option<(u32, u32)> {
422    parse_a1_1based(a1).ok().map(|(row, col, _, _)| (col, row))
423}