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